Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,28 @@ manually.

</MemberCard>

<MemberCard>

##### 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`

</MemberCard>

#### Styles

<MemberCard>
Expand Down
7 changes: 6 additions & 1 deletion src/atoms/surd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
}
Expand Down
7 changes: 5 additions & 2 deletions src/core/atom-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -391,7 +392,8 @@ export class Atom<T extends (Argument | null)[] = (Argument | null)[]> {
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}}`;
}
Expand All @@ -404,7 +406,8 @@ export class Atom<T extends (Argument | null)[] = (Argument | null)[]> {
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}}`;
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/math-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*/
export const _MathEnvironment: {
fractionNavigationOrder: 'denominator-numerator' | 'numerator-denominator';
compactSerialization: boolean;
} = {
fractionNavigationOrder: 'numerator-denominator',
compactSerialization: true,
};
15 changes: 10 additions & 5 deletions src/latex-commands/functions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions src/public/mathfield-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
63 changes: 63 additions & 0 deletions test/compact-serialization.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});