Equations: a multi-letter subscript stays whole after build-up (#1623)

This commit is contained in:
Marco Arnulfo
2026-10-14 19:51:24 -04:00
committed by GitHub
parent 90d8844e44
commit 39af5b17ca
2 changed files with 64 additions and 7 deletions
+49 -7
View File
@@ -499,6 +499,21 @@ fn lin_operand(a: &[MNode], s: &mut String, depth: usize) {
}
}
/// A script or limit operand. The parser reads a script as one letter or a number (`x_10`), so a
/// longer run is grouped: `V_(dd)`, not `V_dd` (which reads back as V_d then d).
fn lin_script(a: &[MNode], s: &mut String, depth: usize) {
if let [MNode::Run(r)] = a
&& r.text.chars().nth(1).is_some()
&& !(r.text.starts_with(|c: char| c.is_ascii_digit()) && r.text.chars().all(|c| c.is_ascii_digit() || c == '.'))
{
s.push('(');
s.push_str(&r.text);
s.push(')');
} else {
lin_operand(a, s, depth);
}
}
fn lin_node(n: &MNode, s: &mut String, depth: usize) {
match n {
MNode::Run(r) => s.push_str(&r.text),
@@ -515,9 +530,9 @@ fn lin_node(n: &MNode, s: &mut String, depth: usize) {
MNode::Script { kind, base, sub, sup } => {
if *kind == ScriptKind::Pre {
s.push('_');
lin_operand(sub, s, depth);
lin_script(sub, s, depth);
s.push('^');
lin_operand(sup, s, depth);
lin_script(sup, s, depth);
s.push('▒');
lin_operand(base, s, depth);
return;
@@ -525,11 +540,11 @@ fn lin_node(n: &MNode, s: &mut String, depth: usize) {
lin_operand(base, s, depth);
if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) {
s.push('_');
lin_operand(sub, s, depth);
lin_script(sub, s, depth);
}
if matches!(kind, ScriptKind::Sup | ScriptKind::SubSup) {
s.push('^');
lin_operand(sup, s, depth);
lin_script(sup, s, depth);
}
}
MNode::Rad { deg, deg_hide, e } => {
@@ -548,11 +563,11 @@ fn lin_node(n: &MNode, s: &mut String, depth: usize) {
s.push(*chr);
if !sub_hide && !sub.is_empty() {
s.push('_');
lin_operand(sub, s, depth);
lin_script(sub, s, depth);
}
if !sup_hide && !sup.is_empty() {
s.push('^');
lin_operand(sup, s, depth);
lin_script(sup, s, depth);
}
s.push('▒');
lin_operand(e, s, depth);
@@ -592,7 +607,7 @@ fn lin_node(n: &MNode, s: &mut String, depth: usize) {
MNode::Lim { upper, e, lim } => {
lin_operand(e, s, depth);
s.push(if *upper { '┴' } else { '┬' });
lin_operand(lim, s, depth);
lin_script(lim, s, depth);
}
MNode::Acc { chr, e } => {
lin_operand(e, s, depth);
@@ -945,6 +960,33 @@ mod tests {
assert!(matches!(&n[2], MNode::Lim { e, lim, .. } if runs(e) == 1 && runs(lim) == 1), "{n:#?}");
}
#[test]
fn multi_letter_scripts_round_trip() {
// A script reads one letter or a number, so `V_dd` is V_d then d: longer scripts are grouped.
let script = |kind, sub: &str, sup: &str| MNode::Script { kind, base: vec![run("V")], sub: parse_linear(sub), sup: parse_linear(sup) };
for n in [
vec![script(ScriptKind::Sub, "dd", "")],
vec![script(ScriptKind::Sup, "", "ab")],
vec![script(ScriptKind::SubSup, "in", "2")],
vec![MNode::Nary {
chr: '∑',
lim_loc: None,
grow: false,
sub_hide: false,
sup_hide: false,
sub: vec![run("ij")],
sup: vec![run("n")],
e: vec![run("x")],
}],
vec![MNode::Lim { upper: false, e: vec![run("⟶")], lim: vec![run("yields")] }],
] {
assert_eq!(parse_linear(&to_linear(&n)), n, "{}", to_linear(&n));
}
// Numbers and single letters stay bare.
assert_eq!(to_linear(&[script(ScriptKind::SubSup, "10", "2.5")]), "V_10^2.5");
assert_eq!(to_linear(&[script(ScriptKind::Sub, "d", "")]), "V_d");
}
#[test]
fn hostile_linear_never_panics() {
for s in ["", "/", "^", "_", "√", "∑", "((((", "))))", "■(", "█(@@&&", "a/", "/b", "^^^__", "√(&)", "∫_^▒", "lim_", "sin"] {
+15
View File
@@ -1199,6 +1199,21 @@ mod tests {
assert!(matches!(eq[0], MNode::Frac { .. }));
}
#[test]
fn build_up_keeps_a_multi_letter_subscript() {
// #1609: `dd` typed in a subscript box split into V_d and d on the next space.
let mut eq: Arg = Vec::new();
let p = insert_nodes(&mut eq, &MathPos::default(), parse_linear("⬚_⬚"));
let p = insert_text(&mut eq, &p, "V", TypeStyle::default());
let p = move_right(&eq, &p).unwrap();
let p = insert_text(&mut eq, &p, "dd", TypeStyle::default());
let p = move_right(&eq, &p).unwrap();
assert_eq!(p, MathPos::new(vec![], 1));
let built = eq.clone();
assert_eq!(build_up(&mut eq, &p, false), None);
assert_eq!(eq, built, "{}", lin(&eq));
}
#[test]
fn backspace_dissolves_and_removes() {
let mut eq = parse_linear("√x");