From e2d8551b7fffc121ab67eb64b6000a8098bc7239 Mon Sep 17 00:00:00 2001 From: Dmitrii Khlapov Date: Sat, 10 Oct 2026 23:41:15 +0200 Subject: [PATCH] Equation gallery: group multi-letter operands in two templates (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A script or above/below operand takes one letter unless it is grouped, as in Word, so `⟶┴yields` (Yields) put only "y" above the arrow and the n×n Matrix's `a_1n`, `a_m1`, `a_mn` subscripted only "1" and "m". Grouped as `⟶┴(yields)` and `a_(1n)`, `a_(m1)`, `a_(mn)`. A new test fails when any template or built-in equation leaves letters right after a script or limit. Claude-Session: https://claude.ai/code/session_01WtHKQ7wrZm3uBuaG1Ye8po Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/engine/src/math_gallery.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/math_gallery.rs b/crates/engine/src/math_gallery.rs index fcf99c843..6c101592d 100644 --- a/crates/engine/src/math_gallery.rs +++ b/crates/engine/src/math_gallery.rs @@ -435,7 +435,7 @@ pub const STRUCTURES: &[Gallery] = &[ t("op.eqAbove", "Text over Equal", "=┴⬚"), ], ), - ("Common Operator Structures", &[t("op.yields", "Yields", "⟶┴yields"), t("op.deltaArrow", "Delta over arrow", "⟶┴∆")]), + ("Common Operator Structures", &[t("op.yields", "Yields", "⟶┴(yields)"), t("op.deltaArrow", "Delta over arrow", "⟶┴∆")]), ], }, Gallery { @@ -486,7 +486,7 @@ pub const STRUCTURES: &[Gallery] = &[ ( "Sparse Matrices", &[ - t("mat.sparse", "n×n Matrix", "(■(a_11&⋯&a_1n@⋮&⋱&⋮@a_m1&⋯&a_mn))"), + t("mat.sparse", "n×n Matrix", "(■(a_11&⋯&a_(1n)@⋮&⋱&⋮@a_(m1)&⋯&a_(mn)))"), t("mat.identityN", "n×n Identity Matrix", "[■(1&0&⋯&0@0&1&⋯&0@⋮&⋮&⋱&⋮@0&0&⋯&1)]"), ], ), @@ -548,6 +548,28 @@ mod tests { }) } + /// A script or above/below operand takes one letter unless it is grouped (`a_(mn)`), as in + /// Word; letters right after one mean a template split a word (`⟶┴yields` put only `y` above + /// the arrow). + fn split_operand(a: &[MNode]) -> bool { + a.windows(2).any(|w| { + matches!(w[0], MNode::Lim { .. } | MNode::Script { .. }) + && matches!(&w[1], MNode::Run(r) if r.text.starts_with(|c: char| c.is_alphabetic())) + }) || a + .iter() + .any(|n| (0..wordcraft_doc::math_edit::child_count(n)).any(|c| wordcraft_doc::math_edit::child(n, c).is_some_and(|x| split_operand(x)))) + } + + #[test] + fn multi_letter_operands_are_grouped() { + for tpl in templates() { + assert!(!split_operand(&tpl.nodes()), "{} splits an operand: {}", tpl.id, tpl.linear); + } + for (id, _, lin) in BUILT_INS { + assert!(!split_operand(&parse_linear(lin)), "{id} splits an operand: {lin}"); + } + } + #[test] fn every_template_builds() { let mut ids = std::collections::HashSet::new();