You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
7.2 KiB
245 lines
7.2 KiB
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org> |
|
// |
|
// This program is free software: you can redistribute it and/or modify it |
|
// under the terms of the GNU General Public License as published by the Free |
|
// Software Foundation, either version 3 of the License, or (at your option) |
|
// any later version. |
|
// |
|
// This program is distributed in the hope that it will be useful, but WITHOUT |
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
|
// more details. |
|
// |
|
// You should have received a copy of the GNU General Public License along with |
|
// this program. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
use once_cell::sync::Lazy; |
|
use regex::Regex; |
|
use rt_format::{ |
|
Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier, |
|
}; |
|
use std::fmt; |
|
|
|
pub enum FmtArg { |
|
I64(i64), |
|
I32(i32), |
|
F64(f64), |
|
F32(f32), |
|
String(String), |
|
} |
|
|
|
impl From<i64> for FmtArg { |
|
fn from(x: i64) -> FmtArg { |
|
FmtArg::I64(x) |
|
} |
|
} |
|
|
|
impl From<i32> for FmtArg { |
|
fn from(x: i32) -> FmtArg { |
|
FmtArg::I32(x) |
|
} |
|
} |
|
|
|
impl From<f64> for FmtArg { |
|
fn from(x: f64) -> FmtArg { |
|
FmtArg::F64(x) |
|
} |
|
} |
|
|
|
impl From<f32> for FmtArg { |
|
fn from(x: f32) -> FmtArg { |
|
FmtArg::F32(x) |
|
} |
|
} |
|
|
|
impl From<&str> for FmtArg { |
|
fn from(x: &str) -> FmtArg { |
|
FmtArg::String(x.to_string()) |
|
} |
|
} |
|
|
|
impl From<String> for FmtArg { |
|
fn from(x: String) -> FmtArg { |
|
FmtArg::String(x) |
|
} |
|
} |
|
|
|
impl ToString for FmtArg { |
|
fn to_string(&self) -> String { |
|
match self { |
|
FmtArg::String(x) => x.clone(), |
|
FmtArg::I64(x) => x.to_string(), |
|
FmtArg::I32(x) => x.to_string(), |
|
FmtArg::F64(x) => x.to_string(), |
|
FmtArg::F32(x) => x.to_string(), |
|
} |
|
} |
|
} |
|
|
|
impl FormatArgument for FmtArg { |
|
fn supports_format(&self, spec: &Specifier) -> bool { |
|
spec.format == Format::Display |
|
} |
|
|
|
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
match self { |
|
Self::String(val) => fmt::Display::fmt(&val, f), |
|
Self::I64(val) => fmt::Display::fmt(&val, f), |
|
Self::I32(val) => fmt::Display::fmt(&val, f), |
|
Self::F64(val) => fmt::Display::fmt(&val, f), |
|
Self::F32(val) => fmt::Display::fmt(&val, f), |
|
} |
|
} |
|
|
|
fn fmt_debug(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_octal(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_lower_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_upper_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_binary(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_lower_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
|
|
fn fmt_upper_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
|
Err(fmt::Error) |
|
} |
|
} |
|
|
|
pub fn rt_format(fmt: &str, arg: FmtArg, clipped_str: &str) -> String { |
|
let arg_string = arg.to_string(); |
|
|
|
if let Ok(pf) = ParsedFormat::parse(fmt, &[arg], &NoNamedArguments) { |
|
let mut s = format!("{}", pf); |
|
|
|
if !clipped_str.is_empty() && !s.contains(arg_string.as_str()) { |
|
remove_last_n_chars(&mut s, clipped_str.chars().count()); |
|
s.push_str(clipped_str); |
|
} |
|
s |
|
} else { |
|
format!("Invalid format string: {}", fmt) |
|
} |
|
} |
|
|
|
fn remove_last_n_chars(s: &mut String, n: usize) { |
|
match s.char_indices().nth_back(n) { |
|
Some((pos, ch)) => s.truncate(pos + ch.len_utf8()), |
|
None => s.clear(), |
|
} |
|
} |
|
|
|
#[test] |
|
fn test_format() { |
|
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), ""), "sway"); |
|
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), "…"), "sway"); |
|
assert_eq!(rt_format("{:.4}", FmtArg::from("𝔰𝔴𝔞𝔶"), "……"), "𝔰𝔴𝔞𝔶"); |
|
|
|
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), ""), "swa"); |
|
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), "…"), "sw…"); |
|
assert_eq!( |
|
rt_format("{:.5}", FmtArg::from("𝔰𝔴𝔞𝔶 𝔴𝔦𝔫𝔡𝔬𝔴"), "…?"), |
|
"𝔰𝔴𝔞…?" |
|
); |
|
assert_eq!( |
|
rt_format("{:.5}", FmtArg::from("sway window"), "..."), |
|
"sw..." |
|
); |
|
assert_eq!(rt_format("{:.2}", FmtArg::from("sway"), "..."), "..."); |
|
} |
|
|
|
pub static PLACEHOLDER_RX: Lazy<Regex> = Lazy::new(|| { |
|
Regex::new( |
|
r"\{(?P<name>[^}:]+)(?::(?P<fmtstr>\{[^}]*\})(?P<clipstr>[^}]*))?\}", |
|
) |
|
.unwrap() |
|
}); |
|
|
|
#[test] |
|
fn test_placeholder_rx() { |
|
let caps = PLACEHOLDER_RX.captures("Hello, {place}!").unwrap(); |
|
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
|
assert_eq!(caps.name("fmtstr"), None); |
|
assert_eq!(caps.name("clipstr"), None); |
|
|
|
let caps = PLACEHOLDER_RX.captures("Hi, {place:{:>10.10}}!").unwrap(); |
|
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
|
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:>10.10}"); |
|
assert_eq!(caps.name("clipstr").unwrap().as_str(), ""); |
|
|
|
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}…}!").unwrap(); |
|
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
|
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
|
assert_eq!(caps.name("clipstr").unwrap().as_str(), "…"); |
|
|
|
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}...}!").unwrap(); |
|
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
|
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
|
assert_eq!(caps.name("clipstr").unwrap().as_str(), "..."); |
|
} |
|
|
|
pub fn maybe_html_escape(do_it: bool, text: String) -> String { |
|
if do_it { |
|
text.replace('<', "<") |
|
.replace('>', ">") |
|
.replace('&', "&") |
|
} else { |
|
text |
|
} |
|
} |
|
|
|
macro_rules! subst_placeholders { |
|
( $fmt_str:expr, $html_escape:expr, |
|
{ $( $($pat:pat_param)|+ => $exp:expr, )+ } |
|
) => { |
|
$crate::shared::fmt::PLACEHOLDER_RX |
|
.replace_all($fmt_str, |caps: ®ex::Captures| { |
|
let value: String = match &caps["name"] { |
|
$( |
|
$( $pat )|+ => { |
|
let val = $crate::shared::fmt::FmtArg::from($exp); |
|
let fmt_str = caps.name("fmtstr") |
|
.map_or("{}", |m| m.as_str()); |
|
let clipped_str = caps.name("clipstr") |
|
.map_or("", |m| m.as_str()); |
|
$crate::shared::fmt::maybe_html_escape( |
|
$html_escape, |
|
$crate::shared::fmt::rt_format(fmt_str, val, clipped_str), |
|
) |
|
} |
|
)+ |
|
_ => caps[0].to_string(), |
|
}; |
|
value |
|
}).into() |
|
}; |
|
} |
|
|
|
pub(crate) use subst_placeholders; |
|
|
|
#[test] |
|
fn test_subst_placeholders() { |
|
let foo = "{a}, {b} = {d}"; |
|
let html_escape = true; |
|
let x: String = subst_placeholders!(foo, html_escape, { |
|
"a" => "1".to_string(), |
|
"b" | "d" => "2".to_string(), |
|
"c" => "3".to_owned(), |
|
}); |
|
|
|
assert_eq!("1, 2 = 2", x); |
|
}
|
|
|