From 9d7c435f6b207e2f2edc15f24fbd6c1f103902b7 Mon Sep 17 00:00:00 2001 From: Mert Dede Date: Tue, 27 Oct 2026 08:33:28 +0300 Subject: [PATCH] #1408: match a Greek final sigma in node.find filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lowercasing a name ending in Σ gives ς, while a query Σ on its own lowercases to σ, so the substring check missed it (CodeRabbit). Both sides now fold ς to σ. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/edit.rs | 18 ++++++++++++------ crates/engine/src/node_find_tests.rs | 10 ++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 151b5820..4ddbd612 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -46,10 +46,16 @@ const LAYER_TYPES: [&str; 16] = [ "SLICE", ]; +/// Lowercase for `node.find`'s substring filters. Greek final sigma becomes σ on both sides, so a +/// query lowercased on its own (`Σ` to `σ`) still matches a name ending in it (`ΟΣ` to `ος`). +fn fold(s: &str) -> String { + s.to_lowercase().replace('ς', "σ") +} + fn node_find(s: &mut Session, p: &Value) -> Result { let cmd = "node.find"; let filter = |key| -> Result> { - get(p, key).map(|v| v.as_str().map(str::to_lowercase).ok_or_else(|| bad(cmd, format!("`{key}` must be a string")))).transpose() + get(p, key).map(|v| v.as_str().map(fold).ok_or_else(|| bad(cmd, format!("`{key}` must be a string")))).transpose() }; let name = filter("name")?; let text = filter("text")?; @@ -77,12 +83,12 @@ fn node_find(s: &mut Session, p: &Value) -> Result { }; let n = doc.get(id)?; stack.push(n.children.iter()); - if name.as_ref().is_some_and(|q| !n.name.to_lowercase().contains(q)) + if name.as_ref().is_some_and(|q| !fold(&n.name).contains(q)) || kind.as_ref().is_some_and(|q| !n.kind.type_name().eq_ignore_ascii_case(q)) - || text.as_ref().is_some_and(|q| !matches!(&n.kind, NodeKind::Text(t) if t.characters.to_lowercase().contains(q))) - || component.as_ref().is_some_and(|q| { - !matches!(&n.kind, NodeKind::Instance { component, .. } if doc.node(*component).is_some_and(|c| c.name.to_lowercase().contains(q))) - }) + || text.as_ref().is_some_and(|q| !matches!(&n.kind, NodeKind::Text(t) if fold(&t.characters).contains(q))) + || component.as_ref().is_some_and( + |q| !matches!(&n.kind, NodeKind::Instance { component, .. } if doc.node(*component).is_some_and(|c| fold(&c.name).contains(q))), + ) { continue; } diff --git a/crates/engine/src/node_find_tests.rs b/crates/engine/src/node_find_tests.rs index 09bb7e63..d8729357 100644 --- a/crates/engine/src/node_find_tests.rs +++ b/crates/engine/src/node_find_tests.rs @@ -35,6 +35,16 @@ fn node_find_filters_are_literal_case_insensitive_and_anded() { found(&mut s, json!({"name": ".*"}), &[]); } +#[test] +fn node_find_matches_a_greek_final_sigma_both_ways() { + let mut s = Session::new(); + let page = s.page().unwrap(); + let os = add(&mut s, page, NodeKind::Text(TextData::new("ΛΟΓΟΣ")), "ΟΣ"); + found(&mut s, json!({"name": "Σ"}), &[os]); + found(&mut s, json!({"name": "ς"}), &[os]); + found(&mut s, json!({"text": "λογοσ"}), &[os]); +} + #[test] fn node_find_scope_is_preorder_strict_descendants_in_stored_order() { let mut s = Session::new();