mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): correct LaTeX whitespace and matrix layouts
This commit is contained in:
parent
8199aca40c
commit
aa601d7ba0
4 changed files with 306 additions and 76 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed LaTeX relation, multiplication, and named-operator spacing, and correctly composed matrices with stacked fractions, operator limits, and adjacent matrices.
|
||||
|
||||
## [0.84.0] - 2026-08-06
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -288,6 +288,90 @@ const DISPLAY_LIMIT_SYMBOLS = new Set([
|
|||
"sum",
|
||||
]);
|
||||
|
||||
const RELATION_COMMANDS = new Set([
|
||||
"Leftarrow",
|
||||
"Leftrightarrow",
|
||||
"Longleftarrow",
|
||||
"Longleftrightarrow",
|
||||
"Longrightarrow",
|
||||
"Rightarrow",
|
||||
"Vdash",
|
||||
"Vvdash",
|
||||
"approx",
|
||||
"asymp",
|
||||
"cong",
|
||||
"dashv",
|
||||
"doteq",
|
||||
"downarrow",
|
||||
"equiv",
|
||||
"ge",
|
||||
"geq",
|
||||
"geqslant",
|
||||
"gets",
|
||||
"gg",
|
||||
"hookleftarrow",
|
||||
"hookrightarrow",
|
||||
"iff",
|
||||
"implies",
|
||||
"in",
|
||||
"leadsto",
|
||||
"le",
|
||||
"leftarrow",
|
||||
"leftharpoondown",
|
||||
"leftharpoonup",
|
||||
"leftrightarrow",
|
||||
"leftrightharpoons",
|
||||
"leq",
|
||||
"leqslant",
|
||||
"ll",
|
||||
"longleftarrow",
|
||||
"longleftrightarrow",
|
||||
"longmapsto",
|
||||
"longrightarrow",
|
||||
"mapsto",
|
||||
"mid",
|
||||
"models",
|
||||
"ne",
|
||||
"nearrow",
|
||||
"neq",
|
||||
"ni",
|
||||
"notin",
|
||||
"nvdash",
|
||||
"nvDash",
|
||||
"nwarrow",
|
||||
"parallel",
|
||||
"perp",
|
||||
"prec",
|
||||
"preceq",
|
||||
"propto",
|
||||
"rightharpoondown",
|
||||
"rightharpoonup",
|
||||
"rightleftharpoons",
|
||||
"rightarrow",
|
||||
"rightsquigarrow",
|
||||
"searrow",
|
||||
"sim",
|
||||
"simeq",
|
||||
"sqsubset",
|
||||
"sqsubseteq",
|
||||
"sqsupset",
|
||||
"sqsupseteq",
|
||||
"subset",
|
||||
"subseteq",
|
||||
"succ",
|
||||
"succeq",
|
||||
"supset",
|
||||
"supseteq",
|
||||
"swarrow",
|
||||
"to",
|
||||
"triangleleft",
|
||||
"triangleright",
|
||||
"twoheadleftarrow",
|
||||
"twoheadrightarrow",
|
||||
"uparrow",
|
||||
"vdash",
|
||||
]);
|
||||
|
||||
const NEGATED_SYMBOLS: Readonly<Record<string, string>> = {
|
||||
"<": "≮",
|
||||
">": "≯",
|
||||
|
|
@ -515,7 +599,7 @@ function replaceCharacters(value: string, replacements: Readonly<Record<string,
|
|||
function formatScript(value: string, kind: "sub" | "sup"): string {
|
||||
value = value.trim();
|
||||
const replacements = kind === "sub" ? SUBSCRIPTS : SUPERSCRIPTS;
|
||||
const unicode = replaceCharacters(value, replacements);
|
||||
const unicode = replaceCharacters(value.replace(/\s*([=+-])\s*/g, "$1"), replacements);
|
||||
if (unicode !== undefined) {
|
||||
return unicode;
|
||||
}
|
||||
|
|
@ -540,8 +624,17 @@ function formatRoot(value: string, symbol = "√"): string {
|
|||
return /^[\p{L}\p{N}.]+$/u.test(value) ? `${symbol}${value}` : `${symbol}(${value})`;
|
||||
}
|
||||
|
||||
const NAMED_OPERATOR_START = "\u{f0004}";
|
||||
const NAMED_OPERATOR_END = "\u{f0005}";
|
||||
const NAMED_OPERATOR_LEFT_SPACING_PATTERN = /(?<=[\p{L}\p{N})\]}\u{f0001}])\u{f0004}/gu;
|
||||
const NAMED_OPERATOR_RIGHT_SPACING_PATTERN = /\u{f0005}(?=[\p{L}\p{N}√\u{f0000}])/gu;
|
||||
|
||||
function normalizeOutput(value: string): string {
|
||||
return value
|
||||
.replace(NAMED_OPERATOR_LEFT_SPACING_PATTERN, " ")
|
||||
.replaceAll(NAMED_OPERATOR_START, "")
|
||||
.replace(NAMED_OPERATOR_RIGHT_SPACING_PATTERN, " ")
|
||||
.replaceAll(NAMED_OPERATOR_END, "")
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/[ \t]+/g, " ").trim())
|
||||
.filter((line, index, lines) => line.length > 0 || (index > 0 && index < lines.length - 1))
|
||||
|
|
@ -562,7 +655,13 @@ interface OperatorNode {
|
|||
upper?: string;
|
||||
}
|
||||
|
||||
type LayoutNode = FractionNode | OperatorNode;
|
||||
interface MatrixNode {
|
||||
type: "matrix";
|
||||
lines: string[];
|
||||
baseline: number;
|
||||
}
|
||||
|
||||
type LayoutNode = FractionNode | OperatorNode | MatrixNode;
|
||||
|
||||
interface Layout {
|
||||
lines: string[];
|
||||
|
|
@ -573,7 +672,8 @@ interface Layout {
|
|||
const LAYOUT_MARKER_START = "\u{f0000}";
|
||||
const LAYOUT_MARKER_END = "\u{f0001}";
|
||||
const LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}/gu;
|
||||
const PROTECTED_SPACE = "\u00a0";
|
||||
const TRAILING_LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}$/u;
|
||||
const PROTECTED_SPACE = "\u{f0002}";
|
||||
|
||||
function padLayoutLine(line: string, width: number, centered = false): string {
|
||||
const padding = Math.max(0, width - visibleWidth(line));
|
||||
|
|
@ -612,18 +712,25 @@ function renderLayout(source: string, nodes: readonly LayoutNode[]): Layout {
|
|||
for (const sourceLine of source.split("\n")) {
|
||||
const layouts: Layout[] = [];
|
||||
let position = 0;
|
||||
let previousWasNode = false;
|
||||
let previousNode: LayoutNode | undefined;
|
||||
for (const match of sourceLine.matchAll(LAYOUT_MARKER_PATTERN)) {
|
||||
const index = match.index;
|
||||
if (index > position) {
|
||||
const sliced = sourceLine.slice(position, index);
|
||||
const text = (previousWasNode ? sliced.trimStart() : sliced).trimEnd();
|
||||
layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
|
||||
}
|
||||
const node = nodes[Number(match[1])];
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
if (index > position) {
|
||||
const sliced = sourceLine.slice(position, index);
|
||||
const trimmed = (previousNode ? sliced.trimStart() : sliced).trimEnd();
|
||||
const preserveLeadingSpace = previousNode?.type === "matrix" && /^\s/.test(sliced);
|
||||
const preserveTrailingSpace = node.type === "matrix" && /\s$/.test(sliced);
|
||||
const text = trimmed
|
||||
? `${preserveLeadingSpace ? " " : ""}${trimmed}${preserveTrailingSpace ? " " : ""}`
|
||||
: preserveLeadingSpace || preserveTrailingSpace
|
||||
? " "
|
||||
: "";
|
||||
layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
|
||||
}
|
||||
if (node.type === "fraction") {
|
||||
const numerator = renderLayout(node.numerator, nodes);
|
||||
const denominator = renderLayout(node.denominator, nodes);
|
||||
|
|
@ -638,7 +745,7 @@ function renderLayout(source: string, nodes: readonly LayoutNode[]): Layout {
|
|||
width,
|
||||
baseline: numerator.lines.length,
|
||||
});
|
||||
} else {
|
||||
} else if (node.type === "operator") {
|
||||
const contentWidth = Math.max(
|
||||
visibleWidth(node.operator),
|
||||
node.lower === undefined ? 0 : visibleWidth(node.lower),
|
||||
|
|
@ -657,13 +764,21 @@ function renderLayout(source: string, nodes: readonly LayoutNode[]): Layout {
|
|||
width: contentWidth + 1,
|
||||
baseline: node.upper === undefined ? 0 : 1,
|
||||
});
|
||||
} else {
|
||||
const width = Math.max(0, ...node.lines.map((line) => visibleWidth(line)));
|
||||
layouts.push({
|
||||
lines: node.lines.map((line) => padLayoutLine(line, width)),
|
||||
width,
|
||||
baseline: node.baseline,
|
||||
});
|
||||
}
|
||||
position = index + match[0].length;
|
||||
previousWasNode = true;
|
||||
previousNode = node;
|
||||
}
|
||||
if (position < sourceLine.length) {
|
||||
const sliced = sourceLine.slice(position);
|
||||
const text = previousWasNode ? sliced.trimStart() : sliced;
|
||||
const trimmed = previousNode ? sliced.trimStart() : sliced;
|
||||
const text = previousNode?.type === "matrix" && /^\s/.test(sliced) ? ` ${trimmed}` : trimmed;
|
||||
layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
|
||||
}
|
||||
const lineLayout = joinLayouts(layouts);
|
||||
|
|
@ -681,14 +796,16 @@ function renderLayout(source: string, nodes: readonly LayoutNode[]): Layout {
|
|||
|
||||
class LatexParser {
|
||||
private readonly source: string;
|
||||
private readonly layoutNodes: LayoutNode[] | undefined;
|
||||
private readonly layoutNodes: LayoutNode[];
|
||||
private readonly display: boolean;
|
||||
private position = 0;
|
||||
private supported = true;
|
||||
private stackFractions = true;
|
||||
|
||||
constructor(source: string, layoutNodes?: LayoutNode[]) {
|
||||
constructor(source: string, layoutNodes: LayoutNode[], display: boolean) {
|
||||
this.source = source;
|
||||
this.layoutNodes = layoutNodes;
|
||||
this.display = display;
|
||||
}
|
||||
|
||||
render(): string | undefined {
|
||||
|
|
@ -723,6 +840,9 @@ class LatexParser {
|
|||
const command = this.parseCommand();
|
||||
if (command === NEGATIVE_SPACE) {
|
||||
result = result.trimEnd();
|
||||
if (result.endsWith(NAMED_OPERATOR_END)) {
|
||||
result = result.slice(0, -NAMED_OPERATOR_END.length);
|
||||
}
|
||||
} else {
|
||||
result += command;
|
||||
}
|
||||
|
|
@ -732,7 +852,12 @@ class LatexParser {
|
|||
if (character === "^" || character === "_") {
|
||||
this.position++;
|
||||
result = result.trimEnd();
|
||||
result += formatScript(this.parseRequiredArgument(false), character === "_" ? "sub" : "sup");
|
||||
const script = formatScript(this.parseRequiredArgument(false), character === "_" ? "sub" : "sup");
|
||||
if (result.endsWith(NAMED_OPERATOR_END)) {
|
||||
result = `${result.slice(0, -NAMED_OPERATOR_END.length)}${script}${NAMED_OPERATOR_END}`;
|
||||
} else {
|
||||
result += script;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -741,6 +866,12 @@ class LatexParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (character === "=" || character === "<" || character === ">") {
|
||||
result = `${result.trimEnd()} ${character} `;
|
||||
this.position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "&") {
|
||||
this.position++;
|
||||
continue;
|
||||
|
|
@ -752,6 +883,17 @@ class LatexParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (character === ".") {
|
||||
const marker = TRAILING_LAYOUT_MARKER_PATTERN.exec(result);
|
||||
const node = marker ? this.layoutNodes[Number(marker[1])] : undefined;
|
||||
if (node?.type === "matrix") {
|
||||
const lastLine = node.lines.length - 1;
|
||||
node.lines[lastLine] = `${node.lines[lastLine] ?? ""}${character}`;
|
||||
this.position++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result += character;
|
||||
this.position++;
|
||||
}
|
||||
|
|
@ -819,14 +961,14 @@ class LatexParser {
|
|||
const value = this.parseRequiredArgument(false).trim();
|
||||
const negated = NEGATED_SYMBOLS[value];
|
||||
if (negated !== undefined) {
|
||||
return negated;
|
||||
return ` ${negated} `;
|
||||
}
|
||||
const characters = Array.from(value);
|
||||
if (characters.length === 0) {
|
||||
this.supported = false;
|
||||
return "";
|
||||
}
|
||||
return `${characters[0]}\u0338${characters.slice(1).join("")}`;
|
||||
return ` ${characters[0]}\u0338${characters.slice(1).join("")} `;
|
||||
}
|
||||
if (LIMIT_OPERATORS.has(command)) {
|
||||
return this.parseOperator(command, "bracket", true, true);
|
||||
|
|
@ -834,10 +976,13 @@ class LatexParser {
|
|||
|
||||
const symbol = SYMBOLS[command];
|
||||
if (symbol !== undefined) {
|
||||
return DISPLAY_LIMIT_SYMBOLS.has(command) ? this.parseOperator(symbol, "script", true) : symbol;
|
||||
if (DISPLAY_LIMIT_SYMBOLS.has(command)) {
|
||||
return this.parseOperator(symbol, "script", true);
|
||||
}
|
||||
return command === "cdot" || command === "times" || RELATION_COMMANDS.has(command) ? ` ${symbol} ` : symbol;
|
||||
}
|
||||
if (NAMED_OPERATORS.has(command)) {
|
||||
return ` ${command} `;
|
||||
return `${NAMED_OPERATOR_START}${command}${NAMED_OPERATOR_END}`;
|
||||
}
|
||||
if (SIZE_COMMANDS.has(command)) {
|
||||
return "";
|
||||
|
|
@ -849,10 +994,10 @@ class LatexParser {
|
|||
return "";
|
||||
}
|
||||
if (command === "frac" || command === "dfrac" || command === "tfrac") {
|
||||
const shouldStack = this.layoutNodes !== undefined && this.stackFractions && command !== "tfrac";
|
||||
const shouldStack = this.display && this.stackFractions && command !== "tfrac";
|
||||
const numerator = this.parseRequiredArgument(!shouldStack);
|
||||
const denominator = this.parseRequiredArgument(!shouldStack);
|
||||
if (shouldStack && this.layoutNodes) {
|
||||
if (shouldStack) {
|
||||
const index =
|
||||
this.layoutNodes.push({
|
||||
type: "fraction",
|
||||
|
|
@ -976,7 +1121,7 @@ class LatexParser {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.layoutNodes && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
|
||||
if (this.display && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
|
||||
const index = this.layoutNodes.push({ type: "operator", operator, lower, upper }) - 1;
|
||||
return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
|
||||
}
|
||||
|
|
@ -1157,36 +1302,39 @@ class LatexParser {
|
|||
return `${cell}${PROTECTED_SPACE.repeat(Math.max(0, (columnWidths[column] ?? 0) - visibleWidth(cell)))}`;
|
||||
}).join(" │ "),
|
||||
);
|
||||
if (environment === "array" || environment === "matrix" || environment === "smallmatrix") {
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
const delimiters: Readonly<Record<string, readonly [string, string, string, string, string, string]>> = {
|
||||
pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
|
||||
bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
|
||||
Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
|
||||
vmatrix: ["│", "│", "│", "│", "│", "│"],
|
||||
Vmatrix: ["║", "║", "║", "║", "║", "║"],
|
||||
};
|
||||
const delimiter = delimiters[environment];
|
||||
if (!delimiter) {
|
||||
this.supported = false;
|
||||
return rows.join("\n");
|
||||
}
|
||||
if (rows.length === 1) {
|
||||
return `${delimiter[0]} ${rows[0]} ${delimiter[1]}`;
|
||||
}
|
||||
return rows
|
||||
.map((row, index) => {
|
||||
let lines: string[];
|
||||
if (environment === "array" || environment === "matrix" || environment === "smallmatrix") {
|
||||
lines = rows;
|
||||
} else {
|
||||
const delimiters: Readonly<Record<string, readonly [string, string, string, string, string, string]>> = {
|
||||
pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
|
||||
bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
|
||||
Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
|
||||
vmatrix: ["│", "│", "│", "│", "│", "│"],
|
||||
Vmatrix: ["║", "║", "║", "║", "║", "║"],
|
||||
};
|
||||
const delimiter = delimiters[environment];
|
||||
if (!delimiter) {
|
||||
this.supported = false;
|
||||
return rows.join("\n");
|
||||
}
|
||||
lines = rows.map((row, index) => {
|
||||
const left = index === 0 ? delimiter[0] : index === rows.length - 1 ? delimiter[4] : delimiter[2];
|
||||
const right = index === 0 ? delimiter[1] : index === rows.length - 1 ? delimiter[5] : delimiter[3];
|
||||
return `${left} ${row} ${right}`;
|
||||
})
|
||||
.join("\n");
|
||||
});
|
||||
}
|
||||
|
||||
if (lines.length <= 1) {
|
||||
return lines[0] ?? "";
|
||||
}
|
||||
const index = this.layoutNodes.push({ type: "matrix", lines, baseline: 0 }) - 1;
|
||||
return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
|
||||
}
|
||||
|
||||
private renderNested(source: string, stackFractions = true): string {
|
||||
const rendered = new LatexParser(source, stackFractions ? this.layoutNodes : undefined).render();
|
||||
const rendered = new LatexParser(source, this.layoutNodes, this.display && stackFractions).render();
|
||||
if (rendered === undefined) {
|
||||
this.supported = false;
|
||||
return source;
|
||||
|
|
@ -1205,12 +1353,12 @@ export interface RenderLatexOptions {
|
|||
* Returns undefined when the expression contains unsupported or malformed syntax.
|
||||
*/
|
||||
export function renderLatex(source: string, options: RenderLatexOptions = {}): string | undefined {
|
||||
const layoutNodes: LayoutNode[] | undefined = options.display ? [] : undefined;
|
||||
const rendered = new LatexParser(source, layoutNodes).render();
|
||||
const layoutNodes: LayoutNode[] = [];
|
||||
const rendered = new LatexParser(source, layoutNodes, options.display === true).render();
|
||||
if (rendered === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (!layoutNodes || layoutNodes.length === 0) {
|
||||
if (layoutNodes.length === 0) {
|
||||
return rendered.replaceAll(PROTECTED_SPACE, " ");
|
||||
}
|
||||
const lines = renderLayout(rendered, layoutNodes).lines;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe("renderLatex", () => {
|
|||
["G = u^2 z + y^2(4+3xy)", "G = u² z + y²(4+3xy)"],
|
||||
["F_1 = uG", "F₁ = uG"],
|
||||
["F_2 = y + 3xG", "F₂ = y + 3xG"],
|
||||
["x=0", "x=0"],
|
||||
["x=0", "x = 0"],
|
||||
["F_2 = F_3 = 0", "F₂ = F₃ = 0"],
|
||||
["xy = -3/2", "xy = -3/2"],
|
||||
["x^2 z = 13/2", "x² z = 13/2"],
|
||||
|
|
@ -94,7 +94,7 @@ describe("renderLatex", () => {
|
|||
defineCases([
|
||||
[
|
||||
String.raw`\det\!\left(\frac{\partial(F_1,F_2,F_3)}{\partial(x,y,z)}\right)=-2.`,
|
||||
"det((∂(F₁,F₂,F₃))/(∂(x,y,z)))=-2.",
|
||||
"det((∂(F₁,F₂,F₃))/(∂(x,y,z))) = -2.",
|
||||
],
|
||||
[
|
||||
String.raw`\begin{aligned}
|
||||
|
|
@ -102,9 +102,9 @@ F(0,0,-\tfrac14)&=(-\tfrac14,0,0),\\
|
|||
F(1,-\tfrac32,\tfrac{13}2)&=(-\tfrac14,0,0),\\
|
||||
F(-1,\tfrac32,\tfrac{13}2)&=(-\tfrac14,0,0).
|
||||
\end{aligned}`,
|
||||
"F(0,0,-1/4)=(-1/4,0,0),\nF(1,-3/2,13/2)=(-1/4,0,0),\nF(-1,3/2,13/2)=(-1/4,0,0).",
|
||||
"F(0,0,-1/4) = (-1/4,0,0),\nF(1,-3/2,13/2) = (-1/4,0,0),\nF(-1,3/2,13/2) = (-1/4,0,0).",
|
||||
],
|
||||
["F=(F_1,F_2,F_3)", "F=(F₁,F₂,F₃)"],
|
||||
["F=(F_1,F_2,F_3)", "F = (F₁,F₂,F₃)"],
|
||||
["F", "F"],
|
||||
["3", "3"],
|
||||
]);
|
||||
|
|
@ -118,7 +118,7 @@ F(-1,\tfrac32,\tfrac{13}2)&=(-\tfrac14,0,0).
|
|||
\frac{\partial f_2}{\partial x} & \frac{\partial f_2}{\partial y} & \frac{\partial f_2}{\partial z} \\
|
||||
\frac{\partial f_3}{\partial x} & \frac{\partial f_3}{\partial y} & \frac{\partial f_3}{\partial z}
|
||||
\end{pmatrix}`,
|
||||
"J = ⎛ (∂ f₁)/(∂ x) │ (∂ f₁)/(∂ y) │ (∂ f₁)/(∂ z) ⎞\n⎜ (∂ f₂)/(∂ x) │ (∂ f₂)/(∂ y) │ (∂ f₂)/(∂ z) ⎟\n⎝ (∂ f₃)/(∂ x) │ (∂ f₃)/(∂ y) │ (∂ f₃)/(∂ z) ⎠",
|
||||
"J = ⎛ (∂ f₁)/(∂ x) │ (∂ f₁)/(∂ y) │ (∂ f₁)/(∂ z) ⎞\n ⎜ (∂ f₂)/(∂ x) │ (∂ f₂)/(∂ y) │ (∂ f₂)/(∂ z) ⎟\n ⎝ (∂ f₃)/(∂ x) │ (∂ f₃)/(∂ y) │ (∂ f₃)/(∂ z) ⎠",
|
||||
],
|
||||
[
|
||||
String.raw`\begin{aligned}
|
||||
|
|
@ -166,7 +166,7 @@ f_3 = x\,(2 - 3u - t)
|
|||
|
||||
describe("extended formulas from a renderer stress-test session", () => {
|
||||
defineCases([
|
||||
[String.raw`e^{i\pi}+1=0`, "e^(iπ)+1=0"],
|
||||
[String.raw`e^{i\pi}+1=0`, "e^(iπ)+1 = 0"],
|
||||
[
|
||||
String.raw`\boxed{
|
||||
\mathcal{Z}(\beta)
|
||||
|
|
@ -193,7 +193,7 @@ R_{\mu\nu}-\frac12 Rg_{\mu\nu}+\Lambda g_{\mu\nu}
|
|||
&=
|
||||
\frac{8\pi G}{c^4}T_{\mu\nu}.
|
||||
\end{aligned}`,
|
||||
"∇_μ T^(μν) = 1/(√(-g)) ∂_μ(√(-g) T^(μν)) +Γ^ν_(μλ)T^(μλ) =0,\nR_(μν)-1/2 Rg_(μν)+Λ g_(μν) = (8π G)/(c⁴)T_(μν).",
|
||||
"∇_μ T^(μν) = 1/(√(-g)) ∂_μ(√(-g) T^(μν)) +Γ^ν_(μλ)T^(μλ) = 0,\nR_(μν)-1/2 Rg_(μν)+Λ g_(μν) = (8π G)/(c⁴)T_(μν).",
|
||||
],
|
||||
[
|
||||
String.raw`f(z)
|
||||
|
|
@ -208,7 +208,11 @@ R_{\mu\nu}-\frac12 Rg_{\mu\nu}+\Lambda g_{\mu\nu}
|
|||
0 & -f & \lambda-g
|
||||
\end{pmatrix}
|
||||
=0.`,
|
||||
"f(z) = 1/(2π i) ∮_γ (f(ζ))/(ζ-z) dζ, det⎛ λ-a │ -b │ 0 ⎞\n⎜ -c │ λ-d │ -e ⎟\n⎝ 0 │ -f │ λ-g ⎠ =0.",
|
||||
[
|
||||
"f(z) = 1/(2π i) ∮_γ (f(ζ))/(ζ-z) dζ, det⎛ λ-a │ -b │ 0 ⎞ = 0.",
|
||||
`${" ".repeat(40)}⎜ -c │ λ-d │ -e ⎟`,
|
||||
`${" ".repeat(40)}⎝ 0 │ -f │ λ-g ⎠`,
|
||||
].join("\n"),
|
||||
],
|
||||
[
|
||||
String.raw`\Psi(x,t)=
|
||||
|
|
@ -226,27 +230,27 @@ c_n
|
|||
\Psi^\ast\Psi, & 0<x<L,\\
|
||||
0, & \text{otherwise}.
|
||||
\end{cases}`,
|
||||
"Ψ(x,t)= ∑ₙ₌₁^∞ cₙ √(2/L) sin((nπ x)/L)_(spatial eigenmode) exp(-(iℏ n²π²)/(2mL²)t), |Ψ(x,t)|² = ⎧ Ψ^∗Ψ if 0<x<L,\n⎩ 0 otherwise.",
|
||||
"Ψ(x,t) = ∑ₙ₌₁^∞ cₙ √(2/L) sin((nπ x)/L)_(spatial eigenmode) exp(-(iℏ n²π²)/(2mL²)t), |Ψ(x,t)|² = ⎧ Ψ^∗Ψ if 0 < x < L,\n⎩ 0 otherwise.",
|
||||
],
|
||||
[String.raw`x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}`, "x=(-b±√(b²-4ac))/(2a)"],
|
||||
[String.raw`\int_0^\infty e^{-x^2}\,dx=\frac{\sqrt{\pi}}{2}`, "∫₀^∞ e^(-x²) dx=(√π)/2"],
|
||||
[String.raw`e^{i\theta}=\cos\theta+i\sin\theta`, "e^(iθ)= cos θ+i sin θ"],
|
||||
[String.raw`\sum_{n=1}^{\infty}\frac{1}{n^2}=\frac{\pi^2}{6}`, "∑ₙ₌₁^∞1/(n²)=π²/6"],
|
||||
[String.raw`\lim_{x\to 0}\frac{\sin x}{x}=1`, "lim[x→0] (sin x)/x=1"],
|
||||
[String.raw`x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}`, "x = (-b±√(b²-4ac))/(2a)"],
|
||||
[String.raw`\int_0^\infty e^{-x^2}\,dx=\frac{\sqrt{\pi}}{2}`, "∫₀^∞ e^(-x²) dx = (√π)/2"],
|
||||
[String.raw`e^{i\theta}=\cos\theta+i\sin\theta`, "e^(iθ) = cos θ+i sin θ"],
|
||||
[String.raw`\sum_{n=1}^{\infty}\frac{1}{n^2}=\frac{\pi^2}{6}`, "∑ₙ₌₁^∞1/(n²) = π²/6"],
|
||||
[String.raw`\lim_{x\to 0}\frac{\sin x}{x}=1`, "lim[x→0] (sin x)/x = 1"],
|
||||
[
|
||||
String.raw`\lim_{n\to\infty}
|
||||
\left(1+\frac{1}{n}\right)^n=e`,
|
||||
"lim[n→∞] (1+1/n)ⁿ=e",
|
||||
"lim[n→∞] (1+1/n)ⁿ = e",
|
||||
],
|
||||
[
|
||||
String.raw`\int_0^1 \frac{x^2}{1+x^3}\,dx
|
||||
=\frac{1}{3}\ln 2`,
|
||||
"∫₀¹ x²/(1+x³) dx =1/3 ln 2",
|
||||
"∫₀¹ x²/(1+x³) dx = 1/3 ln 2",
|
||||
],
|
||||
[
|
||||
String.raw`\sum_{k=1}^{n}\frac{k}{k+1}
|
||||
=n+1-H_{n+1}`,
|
||||
"∑ₖ₌₁ⁿk/(k+1) =n+1-Hₙ₊₁",
|
||||
"∑ₖ₌₁ⁿk/(k+1) = n+1-Hₙ₊₁",
|
||||
],
|
||||
[
|
||||
String.raw`\frac{
|
||||
|
|
@ -266,7 +270,7 @@ c_n
|
|||
\displaystyle \frac{e^x-1}{x}-1
|
||||
}
|
||||
=0`,
|
||||
"lim[x→0] ((sin x)/x-1)/((eˣ-1)/x-1) =0",
|
||||
"lim[x→0] ((sin x)/x-1)/((eˣ-1)/x-1) = 0",
|
||||
],
|
||||
[
|
||||
String.raw`\frac{
|
||||
|
|
@ -307,7 +311,7 @@ c_n
|
|||
renderLatex(String.raw`\epsilon+\varepsilon+\varsigma+\varkappa+\oplus+\otimes+\therefore+\because`),
|
||||
"ϵ+ε+ς+ϰ+⊕+⊗+∴+∵",
|
||||
);
|
||||
assert.strictEqual(renderLatex(String.raw`A\not\subseteq B,\quad x\not\in X`), "A⊈ B, x∉ X");
|
||||
assert.strictEqual(renderLatex(String.raw`A\not\subseteq B,\quad x\not\in X`), "A ⊈ B, x ∉ X");
|
||||
});
|
||||
|
||||
it("renders delimiter commands and invisible delimiters", () => {
|
||||
|
|
@ -315,12 +319,12 @@ c_n
|
|||
renderLatex(String.raw`\lvert{x}\rvert+\lVert{v}\rVert+\left.\frac{dy}{dx}\right|_{x=0}`),
|
||||
"|x|+‖v‖+dy/(dx)|ₓ₌₀",
|
||||
);
|
||||
assert.strictEqual(renderLatex(String.raw`\left\lbrace x \middle| x>0 \right\rbrace`), "{ x | x>0 }");
|
||||
assert.strictEqual(renderLatex(String.raw`\left\lbrace x \middle| x>0 \right\rbrace`), "{ x | x > 0 }");
|
||||
});
|
||||
|
||||
it("renders named, modular, overlaid, and underlaid operators", () => {
|
||||
assert.strictEqual(renderLatex(String.raw`\operatorname*{arg\,max}_{x\in X} f(x)`), "arg max[x∈X] f(x)");
|
||||
assert.strictEqual(renderLatex(String.raw`a\bmod n,\quad a\equiv b\pmod n`), "a mod n, a≡ b (mod n)");
|
||||
assert.strictEqual(renderLatex(String.raw`a\bmod n,\quad a\equiv b\pmod n`), "a mod n, a ≡ b (mod n)");
|
||||
assert.strictEqual(renderLatex(String.raw`\overset{!}{=}+\underset{n}{x}+\stackrel{def}{=}`), "=^!+xₙ+=ᵈᵉᶠ");
|
||||
});
|
||||
|
||||
|
|
@ -339,18 +343,18 @@ c_n
|
|||
it("renders additional display environments", () => {
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\begin{equation}\begin{split}a&=b\\&=c\end{split}\end{equation}`),
|
||||
"a=b\n=c",
|
||||
"a = b\n= c",
|
||||
);
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\begin{alignedat}{2}a&=b&\quad c&=d\\e&=f&g&=h\end{alignedat}`),
|
||||
"a=b c=d\ne=f g=h",
|
||||
"a = b c = d\ne = f g = h",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses natural case conditions and aligns matrix columns", () => {
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\begin{cases}a & x<0 \\ b & \text{if }x=0 \\ c & \text{otherwise}\end{cases}`),
|
||||
"⎧ a if x<0\n⎨ b if x=0\n⎩ c otherwise",
|
||||
"⎧ a if x < 0\n⎨ b if x = 0\n⎩ c otherwise",
|
||||
);
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\begin{pmatrix}1&200\\3000&4\end{pmatrix}`),
|
||||
|
|
@ -358,6 +362,59 @@ c_n
|
|||
);
|
||||
});
|
||||
|
||||
it("composes matrices with fractions and adjacent matrices", () => {
|
||||
assert.strictEqual(
|
||||
renderLatex(
|
||||
String.raw`R\left(\frac{\pi}{4}\right)
|
||||
=
|
||||
\begin{pmatrix}
|
||||
\frac{\sqrt{2}}{2} & -\frac{\sqrt{2}}{2}\\
|
||||
\frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2}
|
||||
\end{pmatrix}.`,
|
||||
{ display: true },
|
||||
),
|
||||
" π\nR( ─ ) = ⎛ (√2)/2 │ -(√2)/2 ⎞\n 4 ⎝ (√2)/2 │ (√2)/2 ⎠.",
|
||||
);
|
||||
assert.strictEqual(
|
||||
renderLatex(
|
||||
String.raw`\mathbf w
|
||||
=
|
||||
R\left(\frac{\pi}{4}\right)
|
||||
\begin{pmatrix}1\\0\end{pmatrix}
|
||||
=
|
||||
\begin{pmatrix}\frac{\sqrt{2}}{2}\\\frac{\sqrt{2}}{2}\end{pmatrix}.`,
|
||||
{ display: true },
|
||||
),
|
||||
" π\nw = R( ─ ) ⎛ 1 ⎞ = ⎛ (√2)/2 ⎞\n 4 ⎝ 0 ⎠ ⎝ (√2)/2 ⎠.",
|
||||
);
|
||||
assert.strictEqual(
|
||||
renderLatex(
|
||||
String.raw`A\mathbf e_1=\begin{pmatrix}\pi\\0\end{pmatrix},\qquad A\mathbf e_2=\begin{pmatrix}0\\\frac{1}{\pi}\end{pmatrix}.`,
|
||||
{ display: true },
|
||||
),
|
||||
"Ae₁ = ⎛ π ⎞, Ae₂ = ⎛ 0 ⎞\n ⎝ 0 ⎠ ⎝ 1/π ⎠.",
|
||||
);
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\sum_{i=0}^n x_i=\begin{pmatrix}a&b\\c&d\end{pmatrix}.`, { display: true }),
|
||||
" n\n ∑ xᵢ = ⎛ a │ b ⎞\ni=0 ⎝ c │ d ⎠.",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes relation, multiplication, and named-operator spacing", () => {
|
||||
for (const source of ["x=y", "x =y", "x=\ny", "x\n=\ny"]) {
|
||||
assert.strictEqual(renderLatex(source), "x = y");
|
||||
}
|
||||
assert.strictEqual(renderLatex("x_{i=0}"), "xᵢ₌₀");
|
||||
assert.strictEqual(renderLatex(String.raw`x\neq0`), "x ≠ 0");
|
||||
assert.strictEqual(renderLatex(String.raw`A\to B`), "A → B");
|
||||
assert.strictEqual(renderLatex(String.raw`\pi\cdot\frac{1}{\pi}`), "π · 1/π");
|
||||
assert.strictEqual(renderLatex(String.raw`\sin\theta`), "sin θ");
|
||||
assert.strictEqual(renderLatex(String.raw`\sin^2 x`), "sin² x");
|
||||
assert.strictEqual(renderLatex(String.raw`-\sin\theta`), "-sin θ");
|
||||
assert.strictEqual(renderLatex(String.raw`i\sin\theta`), "i sin θ");
|
||||
assert.strictEqual(renderLatex(String.raw`\det(A)`), "det(A)");
|
||||
});
|
||||
|
||||
it("stacks operator limits in display mode", () => {
|
||||
assert.strictEqual(renderLatex(String.raw`\sum_{i=0}^n x_i`, { display: true }), " n\n ∑ xᵢ\ni=0");
|
||||
assert.strictEqual(renderLatex(String.raw`\min_{x\in X} f(x)`, { display: true }), "min f(x)\nx∈X");
|
||||
|
|
@ -374,7 +431,7 @@ c_n
|
|||
it("uses the middle brace for intermediate case rows", () => {
|
||||
assert.strictEqual(
|
||||
renderLatex(String.raw`\begin{cases}a & x<0 \\ b & x=0 \\ c & x>0\end{cases}`),
|
||||
"⎧ a if x<0\n⎨ b if x=0\n⎩ c if x>0",
|
||||
"⎧ a if x < 0\n⎨ b if x = 0\n⎩ c if x > 0",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -383,7 +440,7 @@ c_n
|
|||
renderLatex(String.raw`x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}`, {
|
||||
display: true,
|
||||
}),
|
||||
" -b±√(b²-4ac)\nx= ────────────\n 2a",
|
||||
" -b±√(b²-4ac)\nx = ────────────\n 2a",
|
||||
);
|
||||
assert.strictEqual(renderLatex(String.raw`\frac{x^2+1}{x-1}`, { display: true }), "x²+1\n────\nx-1");
|
||||
});
|
||||
|
|
@ -396,7 +453,7 @@ c_n
|
|||
],
|
||||
[
|
||||
String.raw`\lim_{x\to 0}\frac{\frac{\sin x}{x}-1}{\frac{e^x-1}{x}-1}=0`,
|
||||
" (sin x)/x-1\nlim ─────────── =0\nx→0 (eˣ-1)/x-1",
|
||||
" (sin x)/x-1\nlim ─────────── = 0\nx→0 (eˣ-1)/x-1",
|
||||
],
|
||||
[
|
||||
String.raw`\frac{1+\frac{1}{1+\frac{1}{x}}}{1-\frac{1}{1-\frac{1}{x}}}`,
|
||||
|
|
|
|||
|
|
@ -743,6 +743,27 @@ after`,
|
|||
assert.deepStrictEqual(lines, ["Before", "", " 0.1 lux", "E ≈ ────────", " 100 lm/W", "", "after"]);
|
||||
});
|
||||
|
||||
it("aligns matrix rows with the opening delimiter", () => {
|
||||
const markdown = new Markdown(
|
||||
String.raw`Consider the matrix
|
||||
|
||||
\[
|
||||
A=
|
||||
\begin{pmatrix}
|
||||
\pi & 0\\
|
||||
0 & \frac{1}{\pi}
|
||||
\end{pmatrix}.
|
||||
\]`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd());
|
||||
|
||||
assert.deepStrictEqual(lines, ["Consider the matrix", "", "A = ⎛ π │ 0 ⎞", " ⎝ 0 │ 1/π ⎠."]);
|
||||
});
|
||||
|
||||
it("renders lower limits beneath display operators", () => {
|
||||
const markdown = new Markdown(
|
||||
String.raw`\[
|
||||
|
|
@ -755,7 +776,7 @@ after`,
|
|||
|
||||
const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd());
|
||||
|
||||
assert.deepStrictEqual(lines, [" (sin x)/x-1", "lim ─────────── =0", "x→0 (eˣ-1)/x-1"]);
|
||||
assert.deepStrictEqual(lines, [" (sin x)/x-1", "lim ─────────── = 0", "x→0 (eˣ-1)/x-1"]);
|
||||
});
|
||||
|
||||
it("renders math inside lists and tables", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue