diff --git a/src/api.md b/src/api.md
index fd4972613..02c94801e 100644
--- a/src/api.md
+++ b/src/api.md
@@ -803,6 +803,28 @@ manually.
+
+
+##### MathfieldElement.compactSerialization
+
+```ts
+get static compactSerialization(): boolean
+set static compactSerialization(value: boolean): void
+```
+
+When `true` (the default), single-digit arguments are serialized in a
+compact form, without braces: `\frac12`, `\sqrt2`, `x^2`.
+
+When `false`, arguments are always serialized with explicit braces:
+`\frac{1}{2}`, `\sqrt{2}`, `x^{2}`.
+
+The compact form is valid LaTeX, but can be hard to read when a digit
+follows: `\frac123` is one half followed by 3, not 1/23.
+
+**Default**: `true`
+
+
+
#### Styles
diff --git a/src/atoms/surd.ts b/src/atoms/surd.ts
index b9e852924..b3e5419f5 100644
--- a/src/atoms/surd.ts
+++ b/src/atoms/surd.ts
@@ -8,6 +8,7 @@ import { Context } from '../core/context';
import { makeCustomSizedDelim } from '../core/delimiters';
import { latexCommand } from '../core/tokenizer';
+import { _MathEnvironment } from '../core/math-environment';
import { getDefinition } from '../latex-commands/definitions-utils';
import type { CreateAtomOptions, AtomJson, ToLatexOptions } from 'core/types';
@@ -55,7 +56,11 @@ export class SurdAtom extends Atom {
if (this.above && !this.hasEmptyBranch('above'))
return latexCommand(`${command}[${this.aboveToLatex(options)}]`, body);
- if (/^[0-9]$/.test(body)) return `${command}${body}`;
+ // Special case serialization when the radicand is a single digit:
+ // serialize `\sqrt{2}` as `\sqrt2`, unless compact serialization has
+ // been turned off (some people find the compact form confusing).
+ if (_MathEnvironment.compactSerialization && /^[0-9]$/.test(body))
+ return `${command}${body}`;
return latexCommand(command, body);
}
diff --git a/src/core/atom-class.ts b/src/core/atom-class.ts
index aa68b4be9..cb59acc84 100644
--- a/src/core/atom-class.ts
+++ b/src/core/atom-class.ts
@@ -5,6 +5,7 @@ import { boxType, Box } from './box';
import { makeLimitsStack, VBox } from './v-box';
import { joinLatex, latexCommand } from './tokenizer';
import { Mode } from './modes-utils';
+import { _MathEnvironment } from './math-environment';
import { getDefinition } from '../latex-commands/definitions-utils';
import { Context } from './context';
@@ -391,7 +392,8 @@ export class Atom {
else if (sub.length === 1) {
// Using the short form without braces is a stylistic choice
// In general, LaTeX recommends the use of braces
- if (/^[0-9]$/.test(sub)) result += `_${sub}`;
+ if (_MathEnvironment.compactSerialization && /^[0-9]$/.test(sub))
+ result += `_${sub}`;
else result += `_{${sub}}`;
} else result += `_{${sub}}`;
}
@@ -404,7 +406,8 @@ export class Atom {
else if (sup === '\u2033') result += '^\\doubleprime ';
// Using the short form without braces is a stylistic choice
// In general, LaTeX recommends the use of braces
- else if (/^[0-9]$/.test(sup)) result += `^${sup}`;
+ else if (_MathEnvironment.compactSerialization && /^[0-9]$/.test(sup))
+ result += `^${sup}`;
else result += `^{${sup}}`;
} else result += `^{${sup}}`;
}
diff --git a/src/core/math-environment.ts b/src/core/math-environment.ts
index 35197a5b6..dc049faf9 100644
--- a/src/core/math-environment.ts
+++ b/src/core/math-environment.ts
@@ -3,6 +3,8 @@
*/
export const _MathEnvironment: {
fractionNavigationOrder: 'denominator-numerator' | 'numerator-denominator';
+ compactSerialization: boolean;
} = {
fractionNavigationOrder: 'numerator-denominator',
+ compactSerialization: true,
};
diff --git a/src/latex-commands/functions.ts b/src/latex-commands/functions.ts
index 3b6ff7d0b..746997102 100644
--- a/src/latex-commands/functions.ts
+++ b/src/latex-commands/functions.ts
@@ -1,4 +1,5 @@
import { joinLatex, latexCommand } from '../core/tokenizer';
+import { _MathEnvironment } from '../core/math-environment';
import { Atom } from '../core/atom-class';
import { ExtensibleSymbolAtom } from '../atoms/extensible-symbol';
@@ -199,11 +200,15 @@ defineFunction(
serialize: (atom, options) => {
const numer = atom.aboveToLatex(options);
const denom = atom.belowToLatex(options);
- // Special case serialization when numer and denom are digits
- if (/^[0-9]$/.test(numer) && /^[0-9]$/.test(denom))
- // We used to serialize as `\frac{3}{4}` as \frac34, but
- // some people got confused by this, so we now serialize it as `\frac{3}{4}`.
- // See a discussion on this topic here: https://tex.stackexchange.com/questions/82329/how-bad-for-tex-is-omitting-braces-even-if-the-result-is-the-same
+ // Special case serialization when numer and denom are digits:
+ // serialize `\frac{3}{4}` as `\frac34`, unless compact serialization
+ // has been turned off (some people find the compact form confusing).
+ // See a discussion on this topic here: https://tex.stackexchange.com/questions/82329/how-bad-for-tex-is-omitting-braces-even-if-the-result-is-the-same
+ if (
+ _MathEnvironment.compactSerialization &&
+ /^[0-9]$/.test(numer) &&
+ /^[0-9]$/.test(denom)
+ )
return `${atom.command}${numer}${denom}`;
return latexCommand(atom.command, numer, denom);
diff --git a/src/public/mathfield-element.ts b/src/public/mathfield-element.ts
index cc6c78ef7..2f1aa2572 100644
--- a/src/public/mathfield-element.ts
+++ b/src/public/mathfield-element.ts
@@ -1082,6 +1082,26 @@ export class MathfieldElement extends HTMLElement implements Mathfield {
reparseAllMathfields();
}
+ /**
+ * When `true` (the default), single-digit arguments are serialized in a
+ * compact form, without braces: `\frac12`, `\sqrt2`, `x^2`.
+ *
+ * When `false`, arguments are always serialized with explicit braces:
+ * `\frac{1}{2}`, `\sqrt{2}`, `x^{2}`.
+ *
+ * The compact form is valid LaTeX, but can be hard to read when a digit
+ * follows: `\frac123` is one half followed by 3, not 1/23.
+ *
+ * **Default**: `true`
+ * @category Customization
+ */
+ static get compactSerialization(): boolean {
+ return _MathEnvironment.compactSerialization;
+ }
+ static set compactSerialization(value: boolean) {
+ _MathEnvironment.compactSerialization = Boolean(value);
+ }
+
/**
* A custom compute engine instance. If none is provided, a default one is
* used. If `null` is specified, no compute engine is used.
diff --git a/test/compact-serialization.test.ts b/test/compact-serialization.test.ts
new file mode 100644
index 000000000..9fd884dd5
--- /dev/null
+++ b/test/compact-serialization.test.ts
@@ -0,0 +1,63 @@
+import '../src/public/mathlive-ssr';
+import { parseLatex } from '../src/core/parser';
+import { Atom } from '../src/core/atom-class';
+import { _MathEnvironment } from '../src/core/math-environment';
+
+function roundtrip(latex: string): string {
+ const atoms = parseLatex(latex, { parseMode: 'math' });
+ // Discard verbatim LaTeX so the serializer logic is exercised
+ for (const atom of atoms) atom.verbatimLatex = undefined;
+ return Atom.serialize(atoms, { defaultMode: 'math' });
+}
+
+describe('compact serialization', () => {
+ afterEach(() => {
+ _MathEnvironment.compactSerialization = true;
+ });
+
+ test('compact (default)', () => {
+ expect(roundtrip('\\frac{1}{2}')).toBe('\\frac12');
+ expect(roundtrip('\\binom{1}{2}')).toBe('\\binom12');
+ expect(roundtrip('\\sqrt{2}')).toBe('\\sqrt2');
+ expect(roundtrip('x^{2}')).toBe('x^2');
+ expect(roundtrip('x_{1}')).toBe('x_1');
+ });
+
+ test('explicit braces when compactSerialization is false', () => {
+ _MathEnvironment.compactSerialization = false;
+ expect(roundtrip('\\frac{1}{2}')).toBe('\\frac{1}{2}');
+ expect(roundtrip('\\frac{12}{2}')).toBe('\\frac{12}{2}');
+ expect(roundtrip('\\binom{1}{2}')).toBe('\\binom{1}{2}');
+ expect(roundtrip('\\sqrt{2}')).toBe('\\sqrt{2}');
+ expect(roundtrip('\\sqrt{12}')).toBe('\\sqrt{12}');
+ expect(roundtrip('\\sqrt[3]{8}')).toBe('\\sqrt[3]{8}');
+ expect(roundtrip('x^{2}')).toBe('x^{2}');
+ expect(roundtrip('x_{1}')).toBe('x_{1}');
+ expect(roundtrip('x^{12}')).toBe('x^{12}');
+ });
+
+ test('non-digit superscripts are unaffected', () => {
+ expect(roundtrip("x'")).toBe('x^{\\prime}');
+ expect(roundtrip('x^{n}')).toBe('x^{n}');
+ _MathEnvironment.compactSerialization = false;
+ expect(roundtrip("x'")).toBe('x^{\\prime}');
+ expect(roundtrip('x^{n}')).toBe('x^{n}');
+ });
+
+ test('digits are not glued to a following digit', () => {
+ // These round-trip correctly either way, but the compact form is
+ // hard to read: `\frac123` is a half followed by 3, not 1/23
+ _MathEnvironment.compactSerialization = false;
+ expect(roundtrip('\\frac{1}{2}3')).toBe('\\frac{1}{2}3');
+ expect(roundtrip('x^{2}3')).toBe('x^{2}3');
+ expect(roundtrip('x_{1}2')).toBe('x_{1}2');
+ });
+
+ test('`\\sqrt12` is the root of 1, followed by 2', () => {
+ // `\sqrt` takes a single mandatory argument, so `\sqrt12` is
+ // `\sqrt{1}2`, not `\sqrt{12}`
+ expect(roundtrip('\\sqrt12')).toBe('\\sqrt12');
+ _MathEnvironment.compactSerialization = false;
+ expect(roundtrip('\\sqrt12')).toBe('\\sqrt{1}2');
+ });
+});