diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 4712351ca..fd3993e1d 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -488,7 +488,7 @@ fn borders(s: &mut Session, p: &Json) -> Result { } fn format_cells(s: &mut Session, p: &Json) -> Result { - let Some(patch) = p.get("style").cloned() else { + let Some(patch) = p.get("style").map(snake_case_keys) else { // Menu invocation without params: the UI opens the dialog. s.ui_requests.push(crate::UiRequest::Dialog("formatCells".into(), json!({}))); return ok(); @@ -506,6 +506,32 @@ fn format_cells(s: &mut Session, p: &Json) -> Result { }) } +/// The style patch with camelCase keys (`numFmt`, `diagDown`, as documented) renamed to the +/// style's own snake_case (`num_fmt`, `diag_down`). Keys starting with a capital (`Rgb`) stay. +fn snake_case_keys(v: &Json) -> Json { + match v { + Json::Object(m) => Json::Object( + m.iter() + .map(|(k, v)| { + let key = if k.starts_with(|c: char| c.is_ascii_lowercase()) { + k.chars().fold(String::new(), |mut out, c| { + if c.is_ascii_uppercase() { + out.push('_'); + } + out.push(c.to_ascii_lowercase()); + out + }) + } else { + k.clone() + }; + (key, snake_case_keys(v)) + }) + .collect(), + ), + other => other.clone(), + } +} + /// Deep-merges `patch` into `base` (objects merge, everything else replaces). pub(crate) fn merge_json(mut base: Json, patch: &Json) -> Json { match (&mut base, patch) { @@ -1149,6 +1175,20 @@ mod tests { assert_eq!(adjust_decimals("0%", &v, 1), "0.0%"); } + #[test] + fn format_cells_takes_the_documented_keys() { + let mut s = crate::Session::new(); + s.new_workbook(); + s.execute("cell.set", json!({"cell": "A1", "input": "5"})).unwrap(); + s.execute("home.formatCells", json!({"range": "A1", "style": {"numFmt": "0.0", "borders": {"diagUp": {"style": "Thin"}}}})).unwrap(); + let st = &s.execute("cell.get", json!({"cell": "A1"})).unwrap()["style"]; + assert_eq!(st["num_fmt"], "0.0"); + assert_eq!(st["borders"]["diag_up"]["style"], "Thin"); + // The style's own snake_case keys keep working. + s.execute("home.formatCells", json!({"range": "A1", "style": {"num_fmt": "0.00"}})).unwrap(); + assert_eq!(s.execute("cell.get", json!({"cell": "A1"})).unwrap()["style"]["num_fmt"], "0.00"); + } + #[test] fn width_units() { assert_eq!(chars_to_points(8.43), 64.0);