From f58ec399682703e81028432e26ebcaffac942fde Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:19:52 -0500 Subject: [PATCH] [CC3] table.cellAlign takes a horizontal align align: left|center|right|justify sets every paragraph of the target cells; anchor stays the vertical alignment and is optional when align is given. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/table.rs | 41 +++++++++++++++++++++++++++------- crates/engine/src/tests.rs | 23 +++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index 307e46ef..91ac7ac7 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -108,7 +108,7 @@ pub fn specs() -> Vec { "Align Cell Text", ["Table Layout", "Alignment"], None, - "{anchor: top|middle|bottom, cells?, id?}", + "{anchor?: top|middle|bottom (vertical; top when neither anchor nor align is given), align?: left|center|right|justify (horizontal, applied to every paragraph of the cells), cells?, id?}", has_selection, cell_align ), @@ -535,18 +535,43 @@ fn cell_margins(s: &mut Session, p: &Value) -> Result { } fn cell_align(s: &mut Session, p: &Value) -> Result { - use deckcraft_model::text::Anchor; - let a = match str_param(p, "anchor").unwrap_or("top") { - "middle" | "center" | "ctr" => Anchor::Middle, - "bottom" | "b" => Anchor::Bottom, - "top" | "t" => Anchor::Top, - o => return Err(bad("table.cellAlign", format!("unknown anchor {o:?}; one of top, middle, bottom"))), + use deckcraft_model::text::{Align, Anchor}; + // A value that is not a string is refused, not taken as absent. + for k in ["align", "anchor"] { + if p.get(k).is_some_and(|v| !v.is_null() && !v.is_string()) { + return Err(bad("table.cellAlign", format!("`{k}` is a name such as {}", if k == "align" { "left or center" } else { "top or middle" }))); + } + } + // Without `align`, the vertical anchor is the whole command (top when not given). + let align = match str_param(p, "align") { + None => None, + Some("left" | "l") => Some(Align::Left), + Some("center" | "centre" | "ctr") => Some(Align::Center), + Some("right" | "r") => Some(Align::Right), + Some("justify" | "just") => Some(Align::Justify), + Some(o) => return Err(bad("table.cellAlign", format!("unknown align {o:?}; one of left, center, right, justify"))), + }; + let anchor = match (str_param(p, "anchor"), align) { + (None, Some(_)) => None, + (a, _) => Some(match a.unwrap_or("top") { + "middle" | "center" | "ctr" => Anchor::Middle, + "bottom" | "b" => Anchor::Bottom, + "top" | "t" => Anchor::Top, + o => return Err(bad("table.cellAlign", format!("unknown anchor {o:?}; one of top, middle, bottom"))), + }), }; let targets = target_cells(s, p)?; with_table(s, p, "table.cellAlign", move |t| { for (r, c) in targets(t) { if let Some(cell) = t.cell_mut(r, c) { - cell.anchor = Some(a); + if anchor.is_some() { + cell.anchor = anchor; + } + if let Some(a) = align { + for para in &mut cell.text.paragraphs { + para.props.align = Some(a); + } + } } } Ok(()) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4dfc21f4..7bba9ebe 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5646,3 +5646,26 @@ fn invalid_ids_array_never_falls_back_to_the_selection() { assert!(s.doc().unwrap().shape(deckcraft_model::ShapeId(a as u32)).is_some(), "shape deleted for {ids}"); } } + +#[test] +fn table_cell_align_sets_the_horizontal_alignment() { + use deckcraft_model::text::{Align, Anchor}; + let mut s = session(); + let id = table_at(&mut s, &[100.0, 100.0], json!([["a", "b"], ["c", "d"]])); + s.execute("table.cellAlign", &json!({"id": id.0, "align": "center", "cells": [[0, 0], [1, 0]]})).unwrap(); + s.execute("table.cellAlign", &json!({"id": id.0, "align": "right", "anchor": "bottom", "cells": [[0, 1]]})).unwrap(); + let t = table_of(&s, id); + let al = |r, c| t.cell(r, c).unwrap().text.paragraphs.iter().map(|p| p.props.align).collect::>(); + assert_eq!(al(0, 0), vec![Some(Align::Center)]); + assert_eq!(al(1, 0), vec![Some(Align::Center)]); + assert_eq!(al(0, 1), vec![Some(Align::Right)]); + assert_eq!(al(1, 1), vec![None], "other cells are untouched"); + assert_eq!(t.cell(0, 1).unwrap().anchor, Some(Anchor::Bottom)); + assert_eq!(t.cell(0, 0).unwrap().anchor, None, "align alone leaves the vertical anchor"); + assert!(s.execute("table.cellAlign", &json!({"id": id.0, "align": "diagonal"})).is_err()); + // A value that is not a name is refused, not read as absent (which would set the anchor to top). + let rev = s.doc().unwrap().revision; + assert!(s.execute("table.cellAlign", &json!({"id": id.0, "align": 1})).is_err()); + assert!(s.execute("table.cellAlign", &json!({"id": id.0, "align": "left", "anchor": true})).is_err()); + assert_eq!(s.doc().unwrap().revision, rev); +}