From d9d61ee8da04fceb9ed2857e96c8f33f6dc5ff08 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 19:39:35 -0700 Subject: [PATCH] Fields: several capitalization switches apply in the order they appear A field with \* Lower \* Caps applied only \* Lower, so a title of WEEKLY REPORT showed as weekly report. Every capitalization switch in a field code now applies in turn, and the example shows Weekly Report. --- crates/engine/src/cmd/references.rs | 6 ++-- crates/engine/src/cmd/table_formula.rs | 4 +-- crates/layout/src/fields.rs | 40 +++++++++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/cmd/references.rs b/crates/engine/src/cmd/references.rs index c09902053..518e0a511 100644 --- a/crates/engine/src/cmd/references.rs +++ b/crates/engine/src/cmd/references.rs @@ -1145,14 +1145,14 @@ fn new_property_value(instr: &str) -> Option { } /// A count (NUMWORDS, NUMCHARS, REVNUM) in the field's `\*` number format, digits without one, -/// with the field's capitalization switch. +/// with the field's capitalization switches. fn count_text(instr: &str, n: usize) -> String { let fmt = wordcraft_layout::fields::format_switch(instr).unwrap_or(NumFormat::Decimal); wordcraft_layout::fields::field_number(instr, fmt, u32::try_from(n).unwrap_or(u32::MAX)) } -/// A field's computed text with its capitalization switch (`\* Upper`, `\* Lower`, `\* Caps`, -/// `\* FirstCap`) applied. +/// A field's computed text with its capitalization switches (`\* Upper`, `\* Lower`, `\* Caps`, +/// `\* FirstCap`) applied in order. fn text_result(instr: &str, text: String) -> String { wordcraft_layout::fields::apply_case_switch(instr, text) } diff --git a/crates/engine/src/cmd/table_formula.rs b/crates/engine/src/cmd/table_formula.rs index 60d4c02cb..df789911c 100644 --- a/crates/engine/src/cmd/table_formula.rs +++ b/crates/engine/src/cmd/table_formula.rs @@ -109,8 +109,8 @@ pub fn evaluate_field_with( /// with the `\#` picture, or in the cell's language when there is no picture. A number format such /// as `\* CardText`, `\* OrdText` or `\* roman` writes a whole number the format covers (up to /// 999,999 for CardText and OrdText, 1 to 50,000 for roman, 1 to 780 for alphabetic), in lowercase -/// for CardText and OrdText. Any other result keeps its digits. A capitalization switch -/// (`\* Upper`, `\* Lower`, `\* Caps`, `\* FirstCap`) then applies to the text. Other `\*` switches +/// for CardText and OrdText. Any other result keeps its digits. The capitalization switches +/// (`\* Upper`, `\* Lower`, `\* Caps`, `\* FirstCap`) then apply to the text in order. Other `\*` switches /// leave the text as it is. Quoted text, such as a `\#` picture, is not a switch. fn format_switches(switches: &str, value: f64, text: String) -> String { use wordcraft_doc::section::NumFormat; diff --git a/crates/layout/src/fields.rs b/crates/layout/src/fields.rs index 8a0590f2d..ca749dbd8 100644 --- a/crates/layout/src/fields.rs +++ b/crates/layout/src/fields.rs @@ -162,10 +162,12 @@ enum CaseSwitch { FirstCap, } -/// The first capitalization switch in a field code. Quoted text in the code is not a switch. -fn case_switch(instr: &str) -> Option { +/// The capitalization switches in a field code, in the order they appear. Quoted text in the code +/// is not a switch. +fn case_switches(instr: &str) -> Vec { let mut chars = instr.chars().peekable(); let mut quoted = false; + let mut cases = Vec::new(); while let Some(c) = chars.next() { match c { '\\' if quoted => { @@ -186,21 +188,26 @@ fn case_switch(instr: &str) -> Option { "firstcap" => CaseSwitch::FirstCap, _ => continue, }; - return Some(case); + cases.push(case); } _ => {} } } - None + cases } -/// `text` with the field's capitalization switch applied. `\* Upper` capitalizes every letter, -/// `\* Lower` makes every letter lowercase, `\* Caps` capitalizes the first letter of each word, -/// and `\* FirstCap` capitalizes the first letter of the first word. A word's first letter is its -/// first letter or digit, after any leading quote, bracket or other punctuation. Without a switch, -/// the function returns `text` unchanged. +/// `text` with the field's capitalization switches applied in the order they appear, so +/// `\* Lower \* Caps` shows "WEEKLY REPORT" as "Weekly Report". `\* Upper` capitalizes every +/// letter, `\* Lower` makes every letter lowercase, `\* Caps` capitalizes the first letter of each +/// word, and `\* FirstCap` capitalizes the first letter of the first word. A word's first letter is +/// its first letter or digit, after any leading quote, bracket or other punctuation. Without a +/// switch, the function returns `text` unchanged. pub fn apply_case_switch(instr: &str, text: String) -> String { - let Some(case) = case_switch(instr) else { return text }; + case_switches(instr).into_iter().fold(text, apply_case) +} + +/// `text` with one capitalization switch applied (see [`apply_case_switch`]). +fn apply_case(text: String, case: CaseSwitch) -> String { match case { CaseSwitch::Upper => text.to_uppercase(), CaseSwitch::Lower => text.to_lowercase(), @@ -229,7 +236,7 @@ pub fn apply_case_switch(instr: &str, text: String) -> String { } /// Formats `n` in `fmt` as a field shows it. Cardinal and ordinal text is lowercase -/// (`\* CardText` shows "twenty-one"), and the field's capitalization switch applies after that +/// (`\* CardText` shows "twenty-one"), and the field's capitalization switches apply after that /// (`\* CardText \* Caps` shows "Twenty-one"). pub fn field_number(instr: &str, fmt: NumFormat, n: u32) -> String { let text = match fmt { @@ -404,6 +411,17 @@ mod tests { assert_eq!(field_text(r"REF mark \* Upper", "stored", &ctx).0, "stored", "a stored result shows as it is"); } + /// Several capitalization switches apply in the order they appear. + #[test] + fn capitalization_switches_apply_in_order() { + let ctx = FieldCtx { title: "WEEKLY REPORT".into(), ..Default::default() }; + let text = |instr: &str| field_text(instr, "", &ctx).0; + assert_eq!(text(r"TITLE \* Lower \* Caps"), "Weekly Report"); + assert_eq!(text(r"TITLE \* Lower \* FirstCap"), "Weekly report"); + assert_eq!(text(r"TITLE \* Caps \* Lower"), "weekly report"); + assert_eq!(text(r"TITLE \* Lower \* MERGEFORMAT \* Upper"), "WEEKLY REPORT"); + } + /// #1765: ASK and SET only assign text to a bookmark; they show nothing, whatever result is stored. #[test] fn ask_and_set_show_nothing() {