fix: bracket a fraction only where the slash would split it (#20)
This commit was merged in pull request #20.
This commit is contained in:
+71
-3
@@ -611,18 +611,86 @@ function subscript(rendered: string): string {
|
||||
return mapScript(rendered, SUBSCRIPTS) ?? `<sub>${rendered}</sub>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters that split a fraction when a formula is read left to right.
|
||||
*/
|
||||
const OPERAND_SPLIT = /[+−±∓]/;
|
||||
|
||||
/** Prefixes that apply to what follows them, so they are not an operand of their own. */
|
||||
const PREFIX_OPERATORS = new Set(["√", "∛", "∜"]);
|
||||
|
||||
/**
|
||||
* Whether a rendered operand needs brackets beside the fraction slash.
|
||||
*
|
||||
* The rendered text is what a reader sees, so that is what this looks at. Two
|
||||
* cases need brackets and nothing else does: a sum, because the slash would
|
||||
* otherwise divide only its last term, and a denominator that is more than one
|
||||
* thing, because `1/2a` is read as `1/(2a)` by some readers and `(1/2)a` by
|
||||
* others.
|
||||
*
|
||||
* Asking whether an operand is a single token instead, which is what this used
|
||||
* to do, brackets almost everything: `n(n+1)/2` came out as `(n(n+1))/2` and the
|
||||
* quadratic formula as a wall of parentheses.
|
||||
*/
|
||||
function needsFractionBrackets(rendered: string, side: "numerator" | "denominator"): boolean {
|
||||
// A script is skipped rather than read, because `x^{a+b}/y` is already
|
||||
// unambiguous: the sum is above the line by construction.
|
||||
const plain = rendered
|
||||
.replace(/<(?:sup|sub)>[\s\S]*?<\/(?:sup|sub)>/g, "")
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.trim();
|
||||
|
||||
let depth = 0;
|
||||
let operands = 0;
|
||||
|
||||
for (let index = 0; index < plain.length; index += 1) {
|
||||
const char = plain[index];
|
||||
|
||||
if (char === "(" || char === "[" || char === "{") {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === ")" || char === "]" || char === "}") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
continue;
|
||||
}
|
||||
if (depth > 0) continue;
|
||||
|
||||
// A space at the top level is function application or a product, and either
|
||||
// one is clearer bracketed: `(sin x) / x` rather than `sin x/x`.
|
||||
if (char === " ") return true;
|
||||
|
||||
if (OPERAND_SPLIT.test(char)) {
|
||||
// A leading sign belongs to the term rather than splitting the fraction.
|
||||
if (index !== 0) return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// `√π` is one thing; `2π` is two multiplied together.
|
||||
if (PREFIX_OPERATORS.has(char)) continue;
|
||||
|
||||
operands += 1;
|
||||
}
|
||||
|
||||
return side === "denominator" && operands > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* `\frac{a}{b}` as a slash, with brackets only where they change the meaning.
|
||||
*
|
||||
* A stacked fraction is the thing this cannot do, and `1/2` is what a reader
|
||||
* reconstructs from it anyway. `(a+b)/2` needs the brackets; `\alpha/2` does not.
|
||||
* reconstructs from it anyway. The space around the slash appears only when
|
||||
* something beside it is bracketed, where the brackets would otherwise crowd it.
|
||||
*/
|
||||
function renderFraction(numerator: string, denominator: string, display: boolean): string {
|
||||
const top = convert(numerator, display);
|
||||
const bottom = convert(denominator, display);
|
||||
|
||||
const bracket = (rendered: string, body: string) => (isAtomic(body) ? rendered : `(${rendered})`);
|
||||
return `${bracket(top, numerator)}/${bracket(bottom, denominator)}`;
|
||||
const bracketedTop = needsFractionBrackets(top, "numerator");
|
||||
const bracketedBottom = needsFractionBrackets(bottom, "denominator");
|
||||
const slash = bracketedTop || bracketedBottom ? " / " : "/";
|
||||
|
||||
return `${bracketedTop ? `(${top})` : top}${slash}${bracketedBottom ? `(${bottom})` : bottom}`;
|
||||
}
|
||||
|
||||
/** `\sqrt{x}` and `\sqrt[3]{x}`, where only a cube and a fourth root have a glyph. */
|
||||
|
||||
Reference in New Issue
Block a user