mirror of
https://github.com/openai/codex.git
synced 2026-10-21 09:54:56 +00:00
Render aligned equations and optimization notation in TUI math (#47191)
## Why
Aligned equations and common optimization notation previously fell back to TeX source. Scripts without Unicode equivalents also prevented otherwise supported expressions from rendering.
## What changed
- Add display layouts for `aligned` and `\boxed`, preserving fraction geometry and alignment with surrounding math.
- Stack sum limits and `\underset`/`\overset` annotations in display math, with compact forms for inline math and nested arguments.
- Support `\substack`, uppercase `\mathcal`, and single-grapheme `\widehat`.
- Preserve scripts without Unicode equivalents using explicit grouping, such as `x_{gpt}` and `x^{q+1}`.
- Keep layouts bounded and retain source fallback for unsupported input or expressions that do not fit.
## Testing
Add snapshots and regression tests for optimization models, display limits, grouped scripts, fraction alignment, narrow-width fallback, malformed input, and layout bounds. Add streaming coverage comparing character-by-character output with complete rendering at wide and narrow widths in rich and raw modes.
GitOrigin-RevId: 83937880a309375f472228368ebe885d4f50bf9c
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
//! Bounded Unicode layout for a deliberately small TeX math subset.
|
||||
//! Accents apply only to single graphemes so their scope survives terminal rendering.
|
||||
//! Unsupported Unicode scripts use explicit grouping; structured layouts remain bounded.
|
||||
|
||||
use crate::width::display_width;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
mod structured;
|
||||
|
||||
const MAX_ROWS: usize = 16;
|
||||
const MAX_COLUMNS: usize = 256;
|
||||
|
||||
@@ -66,8 +69,9 @@ pub(super) fn render(source: &str, display: bool) -> Option<String> {
|
||||
remaining: source.trim(),
|
||||
depth: 0,
|
||||
display,
|
||||
stack_annotations: display,
|
||||
};
|
||||
let result = parser.sequence(/*group*/ false)?;
|
||||
let result = parser.sequence(SequenceEnd::Input)?;
|
||||
(!result.rows.iter().all(|row| row.trim().is_empty())).then(|| result.rows.join("\n"))
|
||||
}
|
||||
|
||||
@@ -75,6 +79,14 @@ struct MathParser<'a> {
|
||||
remaining: &'a str,
|
||||
depth: usize,
|
||||
display: bool,
|
||||
stack_annotations: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum SequenceEnd {
|
||||
Input,
|
||||
Group,
|
||||
Cell,
|
||||
}
|
||||
|
||||
impl MathParser<'_> {
|
||||
@@ -84,7 +96,7 @@ impl MathParser<'_> {
|
||||
Some(ch)
|
||||
}
|
||||
|
||||
fn sequence(&mut self, group: bool) -> Option<Layout> {
|
||||
fn sequence(&mut self, end: SequenceEnd) -> Option<Layout> {
|
||||
if self.depth >= 32 {
|
||||
return None;
|
||||
}
|
||||
@@ -93,8 +105,18 @@ impl MathParser<'_> {
|
||||
let mut scripts = 0;
|
||||
let mut has_base = false;
|
||||
while let Some(ch) = self.remaining.chars().next() {
|
||||
let remaining = self.remaining.trim_start();
|
||||
if end == SequenceEnd::Cell
|
||||
&& (remaining.starts_with(['&', '}'])
|
||||
|| remaining.starts_with(r"\\")
|
||||
|| remaining.starts_with(r"\end{"))
|
||||
{
|
||||
self.remaining = remaining;
|
||||
self.depth -= 1;
|
||||
return Some(result);
|
||||
}
|
||||
if ch == '}' {
|
||||
if !group {
|
||||
if end != SequenceEnd::Group {
|
||||
return None;
|
||||
}
|
||||
self.take();
|
||||
@@ -120,7 +142,7 @@ impl MathParser<'_> {
|
||||
result = result.join(atom)?;
|
||||
}
|
||||
self.depth -= 1;
|
||||
(!group).then_some(result)
|
||||
(end == SequenceEnd::Input).then_some(result)
|
||||
}
|
||||
|
||||
fn argument(&mut self) -> Option<Layout> {
|
||||
@@ -144,10 +166,10 @@ impl MathParser<'_> {
|
||||
fn atom_inner(&mut self) -> Option<Layout> {
|
||||
let ch = self.take()?;
|
||||
match ch {
|
||||
'{' => self.sequence(/*group*/ true),
|
||||
'{' => self.sequence(SequenceEnd::Group),
|
||||
'\\' => self.command(),
|
||||
'^' | '_' => {
|
||||
let arg = self.argument()?;
|
||||
let arg = self.compact_argument()?;
|
||||
let (plain, alphabet) = if ch == '^' {
|
||||
(
|
||||
"0123456789+-=()abcdefghijklmnoprstuvwxyz",
|
||||
@@ -159,12 +181,17 @@ impl MathParser<'_> {
|
||||
"₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ",
|
||||
)
|
||||
};
|
||||
let mut output = String::new();
|
||||
for value in arg.single()?.chars() {
|
||||
let index = plain.chars().position(|ch| ch == value)?;
|
||||
output.push(alphabet.chars().nth(index)?);
|
||||
}
|
||||
Some(Layout::text(output))
|
||||
let text = arg.single()?;
|
||||
let output: Option<String> = text
|
||||
.chars()
|
||||
.map(|value| {
|
||||
let index = plain.chars().position(|ch| ch == value)?;
|
||||
alphabet.chars().nth(index)
|
||||
})
|
||||
.collect();
|
||||
Some(Layout::text(
|
||||
output.unwrap_or_else(|| format!("{ch}{{{text}}}")),
|
||||
))
|
||||
}
|
||||
'}' | '$' | '%' | '#' | '&' | '`' => None,
|
||||
ch if ch.is_whitespace() => {
|
||||
@@ -195,9 +222,13 @@ impl MathParser<'_> {
|
||||
let name = &self.remaining[..length];
|
||||
self.remaining = &self.remaining[length..];
|
||||
match name {
|
||||
"sum" if self.stack_annotations => self.display_sum(),
|
||||
"begin" | "boxed" | "underset" | "overset" | "substack" | "mathcal" => {
|
||||
self.structured_command(name)
|
||||
}
|
||||
"frac" | "dfrac" | "tfrac" => {
|
||||
let numerator = self.argument()?;
|
||||
let denominator = self.argument()?;
|
||||
let numerator = self.compact_argument()?;
|
||||
let denominator = self.compact_argument()?;
|
||||
if !self.display {
|
||||
return Some(Layout::text(format!(
|
||||
"(({})/({}))",
|
||||
@@ -233,7 +264,7 @@ impl MathParser<'_> {
|
||||
if self.remaining.trim_start().starts_with('[') {
|
||||
return None;
|
||||
}
|
||||
let radicand = self.argument()?;
|
||||
let radicand = self.compact_argument()?;
|
||||
Some(Layout::text(format!("√({})", radicand.single()?)))
|
||||
}
|
||||
"mathbb" => {
|
||||
@@ -249,7 +280,7 @@ impl MathParser<'_> {
|
||||
};
|
||||
Some(Layout::text(text))
|
||||
}
|
||||
"hat" | "bar" | "tilde" | "vec" | "dot" | "ddot" => {
|
||||
"hat" | "widehat" | "bar" | "tilde" | "vec" | "dot" | "ddot" => {
|
||||
let arg = self.argument()?;
|
||||
let text = arg.single()?;
|
||||
if text.graphemes(/*is_extended*/ true).count() != 1
|
||||
@@ -259,7 +290,7 @@ impl MathParser<'_> {
|
||||
return None;
|
||||
}
|
||||
let accent = match name {
|
||||
"hat" => '\u{0302}',
|
||||
"hat" | "widehat" => '\u{0302}',
|
||||
"bar" => '\u{0304}',
|
||||
"tilde" => '\u{0303}',
|
||||
"vec" => '\u{20d7}',
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Small, bounded layouts for aligned equations and common optimization notation.
|
||||
//! Cell parsing shares the parent parser's depth budget and preserves nested groups.
|
||||
//! Aligned blocks compose with surrounding math at their first row's baseline.
|
||||
//! Display annotations and sum limits are centered above or below their base.
|
||||
|
||||
use super::Layout;
|
||||
use super::MAX_COLUMNS;
|
||||
use super::MAX_ROWS;
|
||||
use super::MathParser;
|
||||
use super::SequenceEnd;
|
||||
use crate::width::display_width;
|
||||
|
||||
enum AnnotationPosition {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
fn annotated(self, annotation: Self, position: AnnotationPosition) -> Option<Self> {
|
||||
let width = self.width().max(annotation.width());
|
||||
if self.rows.len() + annotation.rows.len() > MAX_ROWS || width > MAX_COLUMNS {
|
||||
return None;
|
||||
}
|
||||
let baseline = match position {
|
||||
AnnotationPosition::Above => self.baseline + annotation.rows.len(),
|
||||
AnnotationPosition::Below => self.baseline,
|
||||
};
|
||||
let parts = match position {
|
||||
AnnotationPosition::Above => [annotation, self],
|
||||
AnnotationPosition::Below => [self, annotation],
|
||||
};
|
||||
let mut rows = Vec::new();
|
||||
for part in parts {
|
||||
let padding = " ".repeat((width - part.width()) / 2);
|
||||
rows.extend(part.rows.into_iter().map(|row| format!("{padding}{row}")));
|
||||
}
|
||||
Some(Self { rows, baseline })
|
||||
}
|
||||
}
|
||||
|
||||
impl MathParser<'_> {
|
||||
// Single-line child layouts retain compact limits without flattening nested fractions.
|
||||
pub(super) fn compact_argument(&mut self) -> Option<Layout> {
|
||||
let stack_annotations = self.stack_annotations;
|
||||
self.stack_annotations = false;
|
||||
let argument = self.argument();
|
||||
self.stack_annotations = stack_annotations;
|
||||
argument
|
||||
}
|
||||
|
||||
pub(super) fn display_sum(&mut self) -> Option<Layout> {
|
||||
let mut lower = None;
|
||||
let mut upper = None;
|
||||
loop {
|
||||
let remaining = self.remaining.trim_start();
|
||||
let limit = match remaining.chars().next() {
|
||||
Some('_') => &mut lower,
|
||||
Some('^') => &mut upper,
|
||||
_ => break,
|
||||
};
|
||||
if limit.is_some() {
|
||||
return None;
|
||||
}
|
||||
self.remaining = &remaining[1..];
|
||||
*limit = Some(self.compact_argument()?);
|
||||
}
|
||||
// Fix the shared width before centering either limit to avoid cumulative rounding.
|
||||
let width = lower
|
||||
.iter()
|
||||
.chain(&upper)
|
||||
.map(Layout::width)
|
||||
.max()
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
let mut base = Layout::text(format!("{:^width$}", "∑"));
|
||||
if let Some(lower) = lower {
|
||||
base = base.annotated(lower, AnnotationPosition::Below)?;
|
||||
}
|
||||
if let Some(upper) = upper {
|
||||
base = base.annotated(upper, AnnotationPosition::Above)?;
|
||||
}
|
||||
Some(base)
|
||||
}
|
||||
|
||||
pub(super) fn structured_command(&mut self, name: &str) -> Option<Layout> {
|
||||
match name {
|
||||
"begin" => {
|
||||
self.remaining = self.remaining.trim_start().strip_prefix("{aligned}")?;
|
||||
if self.remaining.trim_start().starts_with('[') {
|
||||
return None;
|
||||
}
|
||||
self.aligned()
|
||||
}
|
||||
"boxed" => {
|
||||
if !self.display {
|
||||
return None;
|
||||
}
|
||||
let inner = self.argument()?;
|
||||
let width = inner.width();
|
||||
if inner.rows.len() + 2 > MAX_ROWS || width + 4 > MAX_COLUMNS {
|
||||
return None;
|
||||
}
|
||||
let border = "─".repeat(width + 2);
|
||||
let mut rows = vec![format!("┌{border}┐")];
|
||||
for row in &inner.rows {
|
||||
let padding = " ".repeat(width - display_width(row));
|
||||
rows.push(format!("│ {row}{padding} │"));
|
||||
}
|
||||
rows.push(format!("└{border}┘"));
|
||||
Some(Layout {
|
||||
rows,
|
||||
baseline: inner.baseline + 1,
|
||||
})
|
||||
}
|
||||
"underset" | "overset" => {
|
||||
let annotation = self.compact_argument()?;
|
||||
let base = self.argument()?;
|
||||
if self.stack_annotations {
|
||||
let position = if name == "underset" {
|
||||
AnnotationPosition::Below
|
||||
} else {
|
||||
AnnotationPosition::Above
|
||||
};
|
||||
return base.annotated(annotation, position);
|
||||
}
|
||||
let annotation = annotation.single()?;
|
||||
let base = base.single()?;
|
||||
let marker = if name == "underset" { '_' } else { '^' };
|
||||
// Parenthesize the base so an annotation cannot attach to only its last atom.
|
||||
Some(Layout::text(format!("({base}){marker}{{{annotation}}}")))
|
||||
}
|
||||
"substack" => {
|
||||
self.remaining = self.remaining.trim_start().strip_prefix('{')?;
|
||||
let mut rows = Vec::new();
|
||||
loop {
|
||||
if rows.len() == MAX_ROWS {
|
||||
return None;
|
||||
}
|
||||
self.remaining = self.remaining.trim_start();
|
||||
let row = self.sequence(SequenceEnd::Cell)?;
|
||||
rows.push(row.single()?.trim().to_owned());
|
||||
if let Some(remaining) = self.remaining.strip_prefix('}') {
|
||||
self.remaining = remaining;
|
||||
break;
|
||||
}
|
||||
self.remaining = self.remaining.strip_prefix(r"\\")?;
|
||||
}
|
||||
Some(Layout::text(format!("({})", rows.join("; "))))
|
||||
}
|
||||
"mathcal" => {
|
||||
let arg = self.argument()?;
|
||||
let text: Option<String> = arg
|
||||
.single()?
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
let index = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".find(ch)?;
|
||||
"𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵".chars().nth(index)
|
||||
})
|
||||
.collect();
|
||||
Some(Layout::text(text?))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn aligned(&mut self) -> Option<Layout> {
|
||||
if !self.display {
|
||||
return None;
|
||||
}
|
||||
let mut rows = Vec::new();
|
||||
let mut cells = Vec::new();
|
||||
let mut widths = Vec::<usize>::new();
|
||||
loop {
|
||||
self.remaining = self.remaining.trim_start();
|
||||
// A trailing row separator does not introduce an extra blank row.
|
||||
if cells.is_empty()
|
||||
&& let Some(remaining) = self.remaining.strip_prefix(r"\end{aligned}")
|
||||
{
|
||||
self.remaining = remaining;
|
||||
break;
|
||||
}
|
||||
// Bound empty columns and rows as well as their eventual rendered size.
|
||||
if rows.len() == MAX_ROWS || cells.len() == 16 {
|
||||
return None;
|
||||
}
|
||||
let mut cell = self.sequence(SequenceEnd::Cell)?;
|
||||
if cell.rows.len() == 1 {
|
||||
cell.rows[0] = cell.rows[0].trim().to_owned();
|
||||
}
|
||||
if widths.len() == cells.len() {
|
||||
widths.push(cell.width());
|
||||
} else {
|
||||
widths[cells.len()] = widths[cells.len()].max(cell.width());
|
||||
}
|
||||
cells.push(cell);
|
||||
if let Some(remaining) = self.remaining.strip_prefix('&') {
|
||||
self.remaining = remaining;
|
||||
continue;
|
||||
}
|
||||
if let Some(remaining) = self.remaining.strip_prefix(r"\end{aligned}") {
|
||||
self.remaining = remaining;
|
||||
rows.push(cells);
|
||||
break;
|
||||
}
|
||||
self.remaining = self.remaining.strip_prefix(r"\\")?.trim_start();
|
||||
if let Some(spacing) = self.remaining.strip_prefix('[') {
|
||||
let (distance, remaining) = spacing.split_once(']')?;
|
||||
let number = [
|
||||
"pt", "pc", "in", "bp", "cm", "mm", "dd", "cc", "sp", "em", "ex",
|
||||
]
|
||||
.iter()
|
||||
.find_map(|unit| distance.trim().strip_suffix(unit))?;
|
||||
if !number.trim().parse::<f64>().ok()?.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Physical TeX spacing has no terminal equivalent; retain the row break.
|
||||
self.remaining = remaining;
|
||||
}
|
||||
rows.push(std::mem::take(&mut cells));
|
||||
}
|
||||
let mut output = Vec::new();
|
||||
let mut layout_baseline = 0;
|
||||
for cells in rows {
|
||||
let baseline = cells.iter().map(|cell| cell.baseline).max()?;
|
||||
if output.is_empty() {
|
||||
layout_baseline = baseline;
|
||||
}
|
||||
let height = cells
|
||||
.iter()
|
||||
.map(|cell| baseline + cell.rows.len() - cell.baseline)
|
||||
.max()?;
|
||||
if output.len() + height > MAX_ROWS {
|
||||
return None;
|
||||
}
|
||||
for line_index in 0..height {
|
||||
let mut line = String::new();
|
||||
for (index, cell) in cells.iter().enumerate() {
|
||||
let text = line_index
|
||||
.checked_sub(baseline - cell.baseline)
|
||||
.and_then(|row| cell.rows.get(row))
|
||||
.map(String::as_str)
|
||||
.unwrap_or("");
|
||||
// Align the cell rectangle, preserving numerator/denominator positions.
|
||||
let leading = if index % 2 == 0 {
|
||||
widths[index] - cell.width()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
line.push_str(&" ".repeat(leading));
|
||||
line.push_str(text);
|
||||
line.push_str(&" ".repeat(widths[index] - leading - display_width(text)));
|
||||
if index + 1 < cells.len() {
|
||||
line.push_str(if index % 2 == 0 { " " } else { " " });
|
||||
}
|
||||
}
|
||||
if display_width(&line) > MAX_COLUMNS {
|
||||
return None;
|
||||
}
|
||||
output.push(line.trim_end().to_owned());
|
||||
}
|
||||
}
|
||||
Some(Layout {
|
||||
rows: output,
|
||||
baseline: layout_baseline,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,197 @@ i\hbar \frac{\partial}{\partial t}\Psi(\mathbf r,t)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_optimization_model() {
|
||||
let source = r"Let \(X_{gpt}=x_{gpt}\), with \(X_{gp,-1}=I_{gp}\).
|
||||
|
||||
\[
|
||||
\boxed{
|
||||
\begin{aligned}
|
||||
\underset{z,u,s,m\ge0}{\operatorname{minimize}}\quad
|
||||
&\sum_{g,t}w_{\pi_{gt}}u_{gt}
|
||||
+\lambda\sum_t\sum_{(g,p)\in\mathcal J_t}\kappa_{pt}m_{gpt}
|
||||
+\mu\sum_{(g,t)\in\mathcal Q}s_{gt}
|
||||
\\[2mm]
|
||||
\text{subject to}\quad
|
||||
&\widehat F_{gt}+u_{gt}-s_{gt}=D_{gt}
|
||||
&&\forall g,t
|
||||
\\
|
||||
&\sum_gx_{gpt}\le S_{pt}-B_{pt}
|
||||
&&\forall p,t
|
||||
\\
|
||||
&\sum_{g,p,k}z_{gpk\ell t}\le C_{\ell t}
|
||||
&&\forall \ell,t
|
||||
\\
|
||||
&\sum_{\substack{g\in G_r\\p:\operatorname{region}(p)\in M_r}}x_{gpt}
|
||||
\ge\sum_{g\in G_r,p}\alpha_{rg}x_{gpt}
|
||||
&&\forall r,t
|
||||
\\
|
||||
&q_aL_{adt}\le L_{at}
|
||||
&&\forall a,d,t
|
||||
\\
|
||||
&m_{gpt}\ge X_{gp,t-1}-X_{gpt}
|
||||
&&\forall (g,p)\in\mathcal J_t
|
||||
\end{aligned}}
|
||||
\]
|
||||
|
||||
After the model: \(\alpha^2\).";
|
||||
let rendered = plain(source, /*width*/ 240);
|
||||
assert!(!rendered.contains('\\'), "{rendered}");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_display_limits() {
|
||||
insta::assert_snapshot!(plain(
|
||||
r"\[
|
||||
\underset{x,u,m\ge0}{\operatorname{minimize}}\quad
|
||||
\sum_{g,t}x_{gpt}+\lambda\sum^{N}_{i=1}y_i
|
||||
\]
|
||||
|
||||
\[
|
||||
\sum_{i=1}^N\frac{x_i}{y_i}\overset{\text{def}}{=}\underset{j\in J}{\min} z_j
|
||||
\]
|
||||
|
||||
\[
|
||||
\sum_{j=\sum_i^n i}^N x_j
|
||||
\]
|
||||
|
||||
\[
|
||||
\sum_{ij}^{100}x+\sum^{ij}_{100}y
|
||||
\]
|
||||
|
||||
\[
|
||||
\frac{\sum_i x_i}{\sum_j y_j}+\sqrt{\sum_i x_i}+x^{\sum_i a_i}+x_{\underset{i}{\min}}+\sum_k z_k
|
||||
\]
|
||||
|
||||
Inline: \(\sum_{i=1}^N x_i\), \(\underset{x\ge0}{\min} x\).",
|
||||
/*width*/ 100
|
||||
));
|
||||
assert_eq!(
|
||||
render(r"\sum_i^N x_i", /*display*/ true),
|
||||
render(r"\sum^N_i x_i", /*display*/ true)
|
||||
);
|
||||
for source in [r"\sum_i_j", r"\sum^i^j", r"\sum_", r"\sum^_i"] {
|
||||
assert_eq!(render(source, /*display*/ true), None, "{source}");
|
||||
}
|
||||
let source = r"\[\sum_{i,j,k=1}^{123456789}x_i\]";
|
||||
assert_eq!(
|
||||
plain(source, /*width*/ 12),
|
||||
plain(&format!("`{source}`"), /*width*/ 12)
|
||||
);
|
||||
assert_eq!(
|
||||
render(
|
||||
&format!(
|
||||
r"\begin{{aligned}}{}\end{{aligned}}",
|
||||
r"\sum_i x_i\\".repeat(/*n*/ 9)
|
||||
),
|
||||
/*display*/ true
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_grouped_scripts_and_annotations() {
|
||||
insta::assert_snapshot!(plain(
|
||||
r"Indices: \(x_{gpt} + x_{i,j} + w_{\pi_{gt}} + x^{q+1} + x_{g}^2\).
|
||||
Native scripts: \(x_i^2 + y_{10}\). Sets: \(\mathcal Q_t\).
|
||||
Annotations: \(\underset{i\in I}{\min} x_i\), \(\overset{\text{def}}{=}\).",
|
||||
/*width*/ 100
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_aligned_fraction_and_narrow_fallback() {
|
||||
let formula = r"\[
|
||||
\begin{aligned}
|
||||
\frac{x}{z}&=\frac{1}{2} &&\text{first}\\
|
||||
y_{gpt}&=3 &&\text{second}\\
|
||||
\end{aligned}
|
||||
\]";
|
||||
insta::assert_snapshot!(format!(
|
||||
"Wide:\n{}\n\nNarrow:\n{}",
|
||||
plain(formula, /*width*/ 80),
|
||||
plain(formula, /*width*/ 12)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_aligned_preserves_fraction_geometry() {
|
||||
let source = r"\[
|
||||
\begin{aligned}
|
||||
\frac{x}{y}+\frac{1}{123456789}&=0\\
|
||||
\frac{123456789}{1}+\frac{y}{x}&=1
|
||||
\end{aligned}
|
||||
\]
|
||||
|
||||
\[
|
||||
\begin{aligned}\frac{a}{b}\end{aligned}=c
|
||||
\]
|
||||
|
||||
\[
|
||||
\begin{aligned}x&=\begin{aligned}\frac{a}{b}\end{aligned}\\y&=2\end{aligned}
|
||||
\]";
|
||||
let rendered = plain(source, /*width*/ 80);
|
||||
insta::assert_snapshot!(rendered);
|
||||
let spaced = source
|
||||
.replace('&', " \n &")
|
||||
.replace(r"\\", " \n \\\\")
|
||||
.replace(r"\end{aligned}", " \n \\end{aligned}");
|
||||
assert_eq!(plain(&spaced, /*width*/ 80), rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_structured_bounds_and_invalid_input() {
|
||||
let source = format!(
|
||||
r"\begin{{aligned}}\frac{{a}}{{b}}{}&=0\end{{aligned}}",
|
||||
"x".repeat(/*n*/ 252)
|
||||
);
|
||||
let rendered = render(&source, /*display*/ true).expect("fits the layout width limit");
|
||||
assert_eq!(
|
||||
render(&source.replace('&', " \n &"), /*display*/ true),
|
||||
Some(rendered)
|
||||
);
|
||||
assert_eq!(
|
||||
render(
|
||||
&format!(
|
||||
r"\begin{{aligned}}{}\end{{aligned}}",
|
||||
"x&=1\\\\".repeat(/*n*/ 16)
|
||||
),
|
||||
/*display*/ true
|
||||
),
|
||||
Some(vec!["x =1"; 16].join("\n"))
|
||||
);
|
||||
for source in [
|
||||
r"\begin{aligned}x&=1",
|
||||
r"\begin{aligned}x&=1\end{matrix}",
|
||||
r"\begin{aligned}[b]x&=1\end{aligned}",
|
||||
r"\begin{aligned}x&={a&b}\end{aligned}",
|
||||
r"\begin{aligned}x&=1\\[bad]y&=2\end{aligned}",
|
||||
r"\begin{aligned}x&=1\\[NaNmm]y&=2\end{aligned}",
|
||||
r"\underset{x}",
|
||||
r"\substack{x&y}",
|
||||
] {
|
||||
assert_eq!(render(source, /*display*/ true), None, "{source}");
|
||||
}
|
||||
for source in [
|
||||
format!(
|
||||
r"\begin{{aligned}}{}\end{{aligned}}",
|
||||
"x&=1\\\\".repeat(/*n*/ 17)
|
||||
),
|
||||
format!(
|
||||
r"\begin{{aligned}}{}x\end{{aligned}}",
|
||||
"x&".repeat(/*n*/ 17)
|
||||
),
|
||||
format!(r"\boxed{{{}}}", "x".repeat(/*n*/ 254)),
|
||||
format!("{}x{}", r"\boxed{".repeat(/*n*/ 40), "}".repeat(/*n*/ 40)),
|
||||
format!(r"\substack{{{}}}", "x\\\\".repeat(/*n*/ 17)),
|
||||
] {
|
||||
assert_eq!(render(&source, /*display*/ true), None, "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_narrow_layout_stays_meaningful() {
|
||||
insta::assert_snapshot!(plain(
|
||||
|
||||
@@ -122,7 +122,6 @@ fn unicode_math_bounds_and_unsupported_input() {
|
||||
r"\frac{a}",
|
||||
"{x",
|
||||
"x}",
|
||||
"x^{q}",
|
||||
"^2",
|
||||
"x^2^3",
|
||||
"{a+b}^2",
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
expression: "format!(\"Wide:\\n{}\\n\\nNarrow:\\n{}\", plain(formula, 80), plain(formula, 12))"
|
||||
---
|
||||
Wide:
|
||||
x 1
|
||||
─ =─ first
|
||||
z 2
|
||||
y_{gpt} =3 second
|
||||
|
||||
Narrow:
|
||||
\[
|
||||
\begin{align
|
||||
ed}
|
||||
\frac{x}{z}
|
||||
&=\frac{1}
|
||||
{2}
|
||||
&&\text{firs
|
||||
t}\\
|
||||
y_{gpt}&=3
|
||||
&&\text{seco
|
||||
nd}\\
|
||||
\end{aligned
|
||||
}
|
||||
\]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
expression: "plain(r\"\\[\n\\begin{aligned}\n\\frac{x}{y}+\\frac{1}{123456789}&=0\\\\\n\\frac{123456789}{1}+\\frac{y}{x}&=1\n\\end{aligned}\n\\]\n\n\\[\n\\begin{aligned}\\frac{a}{b}\\end{aligned}=c\n\\]\n\n\\[\n\\begin{aligned}x&=\\begin{aligned}\\frac{a}{b}\\end{aligned}\\\\y&=2\\end{aligned}\n\\]\",\n80)"
|
||||
---
|
||||
x 1
|
||||
─+ ───────── =0
|
||||
y 123456789
|
||||
123456789 y
|
||||
─────────+ ─ =1
|
||||
1 x
|
||||
|
||||
a
|
||||
─=c
|
||||
b
|
||||
|
||||
a
|
||||
x =─
|
||||
b
|
||||
y =2
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
expression: "plain(r\"\\[\n\\underset{x,u,m\\ge0}{\\operatorname{minimize}}\\quad\n\\sum_{g,t}x_{gpt}+\\lambda\\sum^{N}_{i=1}y_i\n\\]\n\n\\[\n\\sum_{i=1}^N\\frac{x_i}{y_i}\\overset{\\text{def}}{=}\\underset{j\\in J}{\\min} z_j\n\\]\n\n\\[\n\\sum_{j=\\sum_i^n i}^N x_j\n\\]\n\n\\[\n\\sum_{ij}^{100}x+\\sum^{ij}_{100}y\n\\]\n\n\\[\n\\frac{\\sum_i x_i}{\\sum_j y_j}+\\sqrt{\\sum_i x_i}+x^{\\sum_i a_i}+x_{\\underset{i}{\\min}}+\\sum_k z_k\n\\]\n\nInline: \\(\\sum_{i=1}^N x_i\\), \\(\\underset{x\\ge0}{\\min} x\\).\",\n100)"
|
||||
---
|
||||
N
|
||||
minimize ∑ x_{gpt}+λ ∑ yᵢ
|
||||
x,u,m≥0 g,t i=1
|
||||
|
||||
N xᵢ def
|
||||
∑ ── = min zⱼ
|
||||
i=1 yᵢ j∈ J
|
||||
|
||||
N
|
||||
∑ xⱼ
|
||||
j=∑ᵢⁿ i
|
||||
|
||||
100 ij
|
||||
∑ x+ ∑ y
|
||||
ij 100
|
||||
|
||||
∑ᵢ xᵢ
|
||||
─────+√(∑ᵢ xᵢ)+x^{∑ᵢ aᵢ}+x_{(min)_{i}}+ ∑ zₖ
|
||||
∑ⱼ yⱼ k
|
||||
|
||||
Inline: ∑ᵢ₌₁^{N} xᵢ, (min)_{x≥0} x.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
expression: "plain(r\"Indices: \\(x_{gpt} + x_{i,j} + w_{\\pi_{gt}} + x^{q+1} + x_{g}^2\\).\nNative scripts: \\(x_i^2 + y_{10}\\). Sets: \\(\\mathcal Q_t\\).\nAnnotations: \\(\\underset{i\\in I}{\\min} x_i\\), \\(\\overset{\\text{def}}{=}\\).\",\n100)"
|
||||
---
|
||||
Indices: x_{gpt} + x_{i,j} + w_{π_{gt}} + x^{q+1} + x_{g}².
|
||||
Native scripts: xᵢ² + y₁₀. Sets: 𝒬ₜ.
|
||||
Annotations: (min)_{i∈ I} xᵢ, (=)^{def}.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
assertion_line: 110
|
||||
expression: rendered
|
||||
---
|
||||
Let X_{gpt}=x_{gpt}, with X_{gp,-1}=I_{gp}.
|
||||
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ minimize ∑ w_{π_{gt}}u_{gt} +λ ∑ ∑ κₚₜm_{gpt} +μ ∑ s_{gt} │
|
||||
│ z,u,s,m≥0 g,t t (g,p)∈𝒥ₜ (g,t)∈𝒬 │
|
||||
│ subject to F̂_{gt}+u_{gt}-s_{gt}=D_{gt} ∀ g,t │
|
||||
│ ∑x_{gpt}≤ Sₚₜ-Bₚₜ ∀ p,t │
|
||||
│ g │
|
||||
│ ∑ z_{gpkℓ t}≤ C_{ℓ t} ∀ ℓ,t │
|
||||
│ g,p,k │
|
||||
│ ∑ x_{gpt} ≥ ∑ α_{rg}x_{gpt} ∀ r,t │
|
||||
│ (g∈ Gᵣ; p:region(p)∈ Mᵣ) g∈ Gᵣ,p │
|
||||
│ qₐL_{adt}≤ Lₐₜ ∀ a,d,t │
|
||||
│ m_{gpt}≥ X_{gp,t-1}-X_{gpt} ∀ (g,p)∈𝒥ₜ │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
After the model: α².
|
||||
@@ -4,6 +4,40 @@ use crate::history_cell::HistoryRenderMode;
|
||||
use crate::markdown::render_streaming_markdown_agent_with_links_and_cwd;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn unicode_math_aligned_stream_matches_complete_render() {
|
||||
let cwd = std::env::temp_dir();
|
||||
let source = "Before.\n\n\\[\n\\begin{aligned}\n\\underset{x}{\\min}&=\\sum_{i=1}^N x_{gpt}\\\\[2mm]\ny&=2\n\\end{aligned}\n\\]\n\nAfter $\\alpha$.\n";
|
||||
for width in [80, 12] {
|
||||
for mode in [HistoryRenderMode::Rich, HistoryRenderMode::Raw] {
|
||||
let mut stream = StreamCore::new(
|
||||
Some(width),
|
||||
&cwd,
|
||||
mode,
|
||||
/*inline_visualization_context*/ None,
|
||||
);
|
||||
let mut emitted = Vec::new();
|
||||
for ch in source.chars() {
|
||||
stream.push_delta(&ch.to_string());
|
||||
emitted.extend(stream.tick_batch(usize::MAX));
|
||||
}
|
||||
let (remaining, raw) = stream.finalize_remaining();
|
||||
emitted.extend(remaining);
|
||||
assert_eq!(raw, source);
|
||||
assert_eq!(
|
||||
emitted,
|
||||
render_source(
|
||||
source,
|
||||
Some(width),
|
||||
&cwd,
|
||||
mode,
|
||||
/*inline_visualization_context*/ None
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_stream_holds_display_until_closed_and_preserves_source() {
|
||||
let cwd = std::env::temp_dir();
|
||||
|
||||
Reference in New Issue
Block a user