| 1 | //! `{placeholder}` templates for the status bar and window title. |
| 2 | //! |
| 3 | //! A template is split into segments at runs of two or more spaces. A segment whose |
| 4 | //! placeholders are all known but one of them is empty drops out with its separator, so |
| 5 | //! `{format} {width}×{height} {index}/{count}` shows just `012/240` in the grid. Unknown |
| 6 | //! names stay as written. |
| 7 | |
| 8 | /// Expand `tpl`, looking placeholders up with `var` (`None` = not a placeholder). |
| 9 | pub fn expand(tpl: &str, var: &dyn Fn(&str) -> Option<String>) -> String { |
| 10 | let mut out: Vec<String> = Vec::new(); |
| 11 | for seg in segments(tpl) { |
| 12 | if let Some(s) = expand_segment(seg, var) { |
| 13 | out.push(s); |
| 14 | } |
| 15 | } |
| 16 | out.join(" ") |
| 17 | } |
| 18 | |
| 19 | /// Split at runs of two or more spaces. |
| 20 | fn segments(tpl: &str) -> Vec<&str> { |
| 21 | let mut out = Vec::new(); |
| 22 | let mut rest = tpl.trim(); |
| 23 | while let Some(i) = rest.find(" ") { |
| 24 | out.push(&rest[..i]); |
| 25 | rest = rest[i..].trim_start(); |
| 26 | } |
| 27 | if !rest.is_empty() { |
| 28 | out.push(rest); |
| 29 | } |
| 30 | out |
| 31 | } |
| 32 | |
| 33 | /// One segment, or `None` if a placeholder in it expanded to nothing. |
| 34 | fn expand_segment(seg: &str, var: &dyn Fn(&str) -> Option<String>) -> Option<String> { |
| 35 | let mut out = String::with_capacity(seg.len()); |
| 36 | let mut rest = seg; |
| 37 | while let Some(open) = rest.find('{') { |
| 38 | out.push_str(&rest[..open]); |
| 39 | let after = &rest[open + 1..]; |
| 40 | match after.find('}') { |
| 41 | Some(close) => { |
| 42 | let name = &after[..close]; |
| 43 | match var(name) { |
| 44 | Some(v) if v.is_empty() => return None, |
| 45 | Some(v) => out.push_str(&v), |
| 46 | None => { |
| 47 | out.push('{'); |
| 48 | out.push_str(name); |
| 49 | out.push('}'); |
| 50 | } |
| 51 | } |
| 52 | rest = &after[close + 1..]; |
| 53 | } |
| 54 | None => { |
| 55 | out.push_str(&rest[open..]); |
| 56 | rest = ""; |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | out.push_str(rest); |
| 61 | (!out.is_empty()).then_some(out) |
| 62 | } |
| 63 | |
| 64 | #[cfg(test)] |
| 65 | mod tests { |
| 66 | use super::*; |
| 67 | |
| 68 | fn vars(name: &str) -> Option<String> { |
| 69 | match name { |
| 70 | "name" => Some("a.png".into()), |
| 71 | "format" | "width" | "height" => Some(String::new()), |
| 72 | "index" => Some("012".into()), |
| 73 | "count" => Some("240".into()), |
| 74 | _ => None, |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | #[test] |
| 79 | fn empty_segments_drop_out() { |
| 80 | assert_eq!( |
| 81 | expand("{format} {width}×{height} {index}/{count}", &vars), |
| 82 | "012/240" |
| 83 | ); |
| 84 | assert_eq!(expand("{name} — remage", &vars), "a.png — remage"); |
| 85 | // Unknown placeholders survive; single spaces don't split. |
| 86 | assert_eq!(expand("{name} {nope} x", &vars), "a.png {nope} x"); |
| 87 | assert_eq!(expand("open {", &vars), "open {"); |
| 88 | assert_eq!(expand("", &vars), ""); |
| 89 | } |
| 90 | } |