mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Expand Unicode math rendering with accents, symbols, and delimiters (#46266)
## What changed - Render `\hat`, `\bar`, `\tilde`, `\vec`, `\dot`, and `\ddot` on single visible graphemes, preserving support for following subscripts and superscripts. - Add symbols for physics, relations, sets, logic, arrows, and integrals, including `\hbar`. - Support named delimiters, including with `\left` and `\right`, plus angle brackets written as `\left<` and `\right>`. - Preserve raw math when accent arguments are empty, invisible, or span multiple graphemes or layout rows. ## Testing Add snapshots for accents, symbols, named delimiters, and the Schrödinger equation, plus rejection tests for ambiguous accent arguments and unsupported delimiters. GitOrigin-RevId: 9dd4883cdf187835a4b4e873886c4299de488705
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
//! Bounded Unicode layout for a deliberately small TeX math subset.
|
||||
//! Accents apply only to single graphemes so their scope survives terminal rendering.
|
||||
|
||||
use crate::width::display_width;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
const MAX_ROWS: usize = 16;
|
||||
const MAX_COLUMNS: usize = 256;
|
||||
@@ -111,7 +113,9 @@ impl MathParser<'_> {
|
||||
let atom = self.atom()?;
|
||||
if !ch.is_whitespace() && ch != '^' && ch != '_' {
|
||||
// Flattening a compound base would change the scope of a following script.
|
||||
has_base = atom.single().is_some_and(|text| text.chars().count() == 1);
|
||||
has_base = atom
|
||||
.single()
|
||||
.is_some_and(|text| text.graphemes(/*is_extended*/ true).count() == 1);
|
||||
}
|
||||
result = result.join(atom)?;
|
||||
}
|
||||
@@ -245,6 +249,26 @@ impl MathParser<'_> {
|
||||
};
|
||||
Some(Layout::text(text))
|
||||
}
|
||||
"hat" | "bar" | "tilde" | "vec" | "dot" | "ddot" => {
|
||||
let arg = self.argument()?;
|
||||
let text = arg.single()?;
|
||||
if text.graphemes(/*is_extended*/ true).count() != 1
|
||||
|| text.trim().is_empty()
|
||||
|| display_width(text) == 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let accent = match name {
|
||||
"hat" => '\u{0302}',
|
||||
"bar" => '\u{0304}',
|
||||
"tilde" => '\u{0303}',
|
||||
"vec" => '\u{20d7}',
|
||||
"dot" => '\u{0307}',
|
||||
"ddot" => '\u{0308}',
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Some(Layout::text(format!("{text}{accent}")))
|
||||
}
|
||||
"mathrm" | "mathbf" | "mathit" => self.argument(),
|
||||
"text" | "operatorname" => {
|
||||
self.remaining = self.remaining.trim_start().strip_prefix('{')?;
|
||||
@@ -263,6 +287,19 @@ impl MathParser<'_> {
|
||||
match self.take()? {
|
||||
'.' => Some(Layout::text("")),
|
||||
ch @ ('(' | ')' | '[' | ']' | '|') => Some(Layout::text(ch.to_string())),
|
||||
'<' => Some(Layout::text("⟨")),
|
||||
'>' => Some(Layout::text("⟩")),
|
||||
'\\' => {
|
||||
let length = self
|
||||
.remaining
|
||||
.bytes()
|
||||
.take_while(u8::is_ascii_alphabetic)
|
||||
.count()
|
||||
.max(/*other*/ 1);
|
||||
let (name, remaining) = self.remaining.split_at_checked(length)?;
|
||||
self.remaining = remaining;
|
||||
delimiter(name).map(Layout::text)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -289,13 +326,17 @@ fn symbol(name: &str) -> Option<&'static str> {
|
||||
"vartheta" => "ϑ",
|
||||
"iota" => "ι",
|
||||
"kappa" => "κ",
|
||||
"varkappa" => "ϰ",
|
||||
"lambda" => "λ",
|
||||
"mu" => "μ",
|
||||
"nu" => "ν",
|
||||
"xi" => "ξ",
|
||||
"pi" => "π",
|
||||
"varpi" => "ϖ",
|
||||
"rho" => "ρ",
|
||||
"varrho" => "ϱ",
|
||||
"sigma" => "σ",
|
||||
"varsigma" => "ς",
|
||||
"tau" => "τ",
|
||||
"upsilon" => "υ",
|
||||
"phi" => "ϕ",
|
||||
@@ -316,35 +357,103 @@ fn symbol(name: &str) -> Option<&'static str> {
|
||||
"Omega" => "Ω",
|
||||
"sum" => "∑",
|
||||
"prod" => "∏",
|
||||
"coprod" => "∐",
|
||||
"int" => "∫",
|
||||
"iint" => "∬",
|
||||
"iiint" => "∭",
|
||||
"oint" => "∮",
|
||||
"infty" => "∞",
|
||||
"partial" => "∂",
|
||||
"nabla" => "∇",
|
||||
"hbar" => "ℏ",
|
||||
"ell" => "ℓ",
|
||||
"Re" => "ℜ",
|
||||
"Im" => "ℑ",
|
||||
"aleph" => "ℵ",
|
||||
"imath" => "ı",
|
||||
"jmath" => "ȷ",
|
||||
"prime" => "′",
|
||||
"angle" => "∠",
|
||||
"dagger" => "†",
|
||||
"ddagger" => "‡",
|
||||
"pm" => "±",
|
||||
"mp" => "∓",
|
||||
"times" => "×",
|
||||
"cdot" => "·",
|
||||
"div" => "÷",
|
||||
"circ" => "∘",
|
||||
"bullet" => "∙",
|
||||
"oplus" => "⊕",
|
||||
"otimes" => "⊗",
|
||||
"odot" => "⊙",
|
||||
"le" | "leq" => "≤",
|
||||
"ge" | "geq" => "≥",
|
||||
"ne" | "neq" => "≠",
|
||||
"approx" => "≈",
|
||||
"propto" => "∝",
|
||||
"sim" => "∼",
|
||||
"simeq" => "≃",
|
||||
"cong" => "≅",
|
||||
"lesssim" => "≲",
|
||||
"gtrsim" => "≳",
|
||||
"ll" => "≪",
|
||||
"gg" => "≫",
|
||||
"perp" => "⊥",
|
||||
"parallel" => "∥",
|
||||
"equiv" => "≡",
|
||||
"in" => "∈",
|
||||
"notin" => "∉", // codespell:ignore notin
|
||||
"ni" => "∋",
|
||||
"subset" => "⊂",
|
||||
"subseteq" => "⊆",
|
||||
"supset" => "⊃",
|
||||
"supseteq" => "⊇",
|
||||
"cup" => "∪",
|
||||
"cap" => "∩",
|
||||
"emptyset" => "∅",
|
||||
"bigcup" => "⋃",
|
||||
"bigcap" => "⋂",
|
||||
"setminus" => "∖",
|
||||
"emptyset" | "varnothing" => "∅",
|
||||
"land" | "wedge" => "∧",
|
||||
"lor" | "vee" => "∨",
|
||||
"neg" | "lnot" => "¬",
|
||||
"top" => "⊤",
|
||||
"bot" => "⊥",
|
||||
"forall" => "∀",
|
||||
"exists" => "∃",
|
||||
"nexists" => "∄",
|
||||
"to" | "rightarrow" => "→",
|
||||
"leftarrow" => "←",
|
||||
"Rightarrow" => "⇒",
|
||||
"Leftrightarrow" => "⇔",
|
||||
"leftrightarrow" => "↔",
|
||||
"mapsto" => "↦",
|
||||
"uparrow" => "↑",
|
||||
"downarrow" => "↓",
|
||||
"updownarrow" => "↕",
|
||||
"Leftarrow" | "impliedby" => "⇐",
|
||||
"Rightarrow" | "implies" => "⇒",
|
||||
"Leftrightarrow" | "iff" => "⇔",
|
||||
"ldots" | "dots" => "…",
|
||||
"cdots" => "⋯",
|
||||
"vdots" => "⋮",
|
||||
"ddots" => "⋱",
|
||||
_ => return delimiter(name),
|
||||
})
|
||||
}
|
||||
|
||||
fn delimiter(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"langle" => "⟨",
|
||||
"rangle" => "⟩",
|
||||
"lbrace" | "{" => "{",
|
||||
"rbrace" | "}" => "}",
|
||||
"lbrack" => "[",
|
||||
"rbrack" => "]",
|
||||
"vert" | "lvert" | "rvert" => "|",
|
||||
"Vert" | "lVert" | "rVert" | "|" => "‖",
|
||||
"lfloor" => "⌊",
|
||||
"rfloor" => "⌋",
|
||||
"lceil" => "⌈",
|
||||
"rceil" => "⌉",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,6 +56,20 @@ $$\beta$$";
|
||||
insta::assert_snapshot!(plain(source, /*width*/ 80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_schrodinger_equation_snapshot() {
|
||||
insta::assert_snapshot!(plain(
|
||||
r"\[
|
||||
i\hbar \frac{\partial}{\partial t}\Psi(\mathbf r,t)
|
||||
=
|
||||
\hat H\Psi(\mathbf r,t)
|
||||
=
|
||||
\left[-\frac{\hbar^2}{2m}\nabla^2+V(\mathbf r,t)\right]\Psi(\mathbf r,t).
|
||||
\]",
|
||||
/*width*/ 100
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_narrow_layout_stays_meaningful() {
|
||||
insta::assert_snapshot!(plain(
|
||||
|
||||
@@ -31,6 +31,60 @@ fn unicode_math_inline_narrow_snapshot() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_accents_and_symbols_snapshot() {
|
||||
insta::assert_snapshot!(plain(
|
||||
r"Operators: $\hat H^2 + \hbar^2$, $\ell$, $A\dagger$, $B\ddagger$.
|
||||
Accents: $\bar{x}_1$, $\tilde{x}^2$, $\vec{v}_i$, $\dot{x}$, $\ddot{x}$.
|
||||
Greek accents: $\hat{\Psi}^2$, $\bar{\varrho}$, $\tilde{\varsigma}$.
|
||||
Other symbols: $\varkappa+\varpi$, $\Re z+\Im z$, $\aleph_0$, $\hat{\imath}$.
|
||||
Relations: $a\propto b\sim c\simeq d\ll e\gg f$.
|
||||
Geometry: $a\perp b\parallel c$, $\angle ABC\cong\angle DEF$.
|
||||
Bounds: $a\lesssim b\gtrsim c$; operators: $a\oplus b\otimes c\odot d$.
|
||||
Sets: $A\supseteq B\supset C\ni x$, $A\setminus B=\varnothing$.
|
||||
Logic: $\neg P\land Q\lor R\implies S\iff T\impliedby U$, $\nexists x$.
|
||||
Arrows: $a\leftrightarrow b\mapsto c\Leftarrow d$, $\uparrow\downarrow\updownarrow$.
|
||||
Integrals: $\iint f$, $\iiint g$, $\oint h$.
|
||||
Collections: $\coprod A$, $\bigcup B$, $\bigcap C$.
|
||||
Punctuation: $x\prime$, $a\circ b\bullet c$, $\vdots\ddots$.",
|
||||
/*width*/ 80
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_named_delimiters_snapshot() {
|
||||
insta::assert_snapshot!(plain(
|
||||
r"Inner product: $\left\langle\hat H\right\rangle$.
|
||||
Rounding: $\lfloor x\rfloor + \left\lceil y\right\rceil$.
|
||||
Norms: $\left\lVert v\right\rVert$, $\lvert x\rvert$, $\left\|w\right\|$.
|
||||
Sets: $\left\{x\right\}$, $\lbrace y\rbrace$, $\lbrack z\rbrack$.
|
||||
Angles: $\left<x\right>$.",
|
||||
/*width*/ 80
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_accents_reject_ambiguous_arguments() {
|
||||
for source in [
|
||||
r"\hat{xy}",
|
||||
r"\bar{x+y}",
|
||||
r"\tilde{x^2}",
|
||||
r"\vec{\frac{x}{y}}",
|
||||
r"\dot{}",
|
||||
r"\ddot{ }",
|
||||
r"\hat",
|
||||
"\\hat{\u{0302}}",
|
||||
] {
|
||||
for display in [false, true] {
|
||||
assert_eq!(render(source, display), None, "{source}");
|
||||
}
|
||||
assert_eq!(
|
||||
plain(&format!("\\({source}\\)"), /*width*/ 80),
|
||||
format!("\\({source}\\)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_math_preserves_markdown_contexts() {
|
||||
for (source, expected) in [
|
||||
@@ -77,6 +131,8 @@ fn unicode_math_bounds_and_unsupported_input() {
|
||||
r"\sqrt[3]{x}",
|
||||
r"\sqrt [3]{x}",
|
||||
r"\left x",
|
||||
r"\left\alpha x\right\rangle",
|
||||
r"\left\ ",
|
||||
r"\text{\alpha}",
|
||||
r"\begin{matrix}a&b\end{matrix}",
|
||||
] {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_display_tests.rs
|
||||
expression: "plain(r\"\\[\ni\\hbar \\frac{\\partial}{\\partial t}\\Psi(\\mathbf r,t)\n=\n\\hat H\\Psi(\\mathbf r,t)\n=\n\\left[-\\frac{\\hbar^2}{2m}\\nabla^2+V(\\mathbf r,t)\\right]\\Psi(\\mathbf r,t).\n\\]\",\n100)"
|
||||
---
|
||||
∂ ℏ²
|
||||
iℏ ───Ψ(r,t) = ĤΨ(r,t) = [- ──∇²+V(r,t)]Ψ(r,t).
|
||||
∂ t 2m
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_tests.rs
|
||||
expression: "plain(r\"Operators: $\\hat H^2 + \\hbar^2$, $\\ell$, $A\\dagger$, $B\\ddagger$.\nAccents: $\\bar{x}_1$, $\\tilde{x}^2$, $\\vec{v}_i$, $\\dot{x}$, $\\ddot{x}$.\nGreek accents: $\\hat{\\Psi}^2$, $\\bar{\\varrho}$, $\\tilde{\\varsigma}$.\nOther symbols: $\\varkappa+\\varpi$, $\\Re z+\\Im z$, $\\aleph_0$, $\\hat{\\imath}$.\nRelations: $a\\propto b\\sim c\\simeq d\\ll e\\gg f$.\nGeometry: $a\\perp b\\parallel c$, $\\angle ABC\\cong\\angle DEF$.\nBounds: $a\\lesssim b\\gtrsim c$; operators: $a\\oplus b\\otimes c\\odot d$.\nSets: $A\\supseteq B\\supset C\\ni x$, $A\\setminus B=\\varnothing$.\nLogic: $\\neg P\\land Q\\lor R\\implies S\\iff T\\impliedby U$, $\\nexists x$.\nArrows: $a\\leftrightarrow b\\mapsto c\\Leftarrow d$, $\\uparrow\\downarrow\\updownarrow$.\nIntegrals: $\\iint f$, $\\iiint g$, $\\oint h$.\nCollections: $\\coprod A$, $\\bigcup B$, $\\bigcap C$.\nPunctuation: $x\\prime$, $a\\circ b\\bullet c$, $\\vdots\\ddots$.\",\n80)"
|
||||
---
|
||||
Operators: Ĥ² + ℏ², ℓ, A†, B‡.
|
||||
Accents: x̄₁, x̃², v⃗ᵢ, ẋ, ẍ.
|
||||
Greek accents: Ψ̂², ϱ̄, ς̃.
|
||||
Other symbols: ϰ+ϖ, ℜ z+ℑ z, ℵ₀, ı̂.
|
||||
Relations: a∝ b∼ c≃ d≪ e≫ f.
|
||||
Geometry: a⊥ b∥ c, ∠ ABC≅∠ DEF.
|
||||
Bounds: a≲ b≳ c; operators: a⊕ b⊗ c⊙ d.
|
||||
Sets: A⊇ B⊃ C∋ x, A∖ B=∅.
|
||||
Logic: ¬ P∧ Q∨ R⇒ S⇔ T⇐ U, ∄ x.
|
||||
Arrows: a↔ b↦ c⇐ d, ↑↓↕.
|
||||
Integrals: ∬ f, ∭ g, ∮ h.
|
||||
Collections: ∐ A, ⋃ B, ⋂ C.
|
||||
Punctuation: x′, a∘ b∙ c, ⋮⋱.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/markdown_render/math_tests.rs
|
||||
expression: "plain(r\"Inner product: $\\left\\langle\\hat H\\right\\rangle$.\nRounding: $\\lfloor x\\rfloor + \\left\\lceil y\\right\\rceil$.\nNorms: $\\left\\lVert v\\right\\rVert$, $\\lvert x\\rvert$, $\\left\\|w\\right\\|$.\nSets: $\\left\\{x\\right\\}$, $\\lbrace y\\rbrace$, $\\lbrack z\\rbrack$.\nAngles: $\\left<x\\right>$.\",\n80)"
|
||||
---
|
||||
Inner product: ⟨Ĥ⟩.
|
||||
Rounding: ⌊ x⌋ + ⌈ y⌉.
|
||||
Norms: ‖ v‖, | x|, ‖w‖.
|
||||
Sets: {x}, { y}, [ z].
|
||||
Angles: ⟨x⟩.
|
||||
Reference in New Issue
Block a user