use serde_json::Value;
use std::collections::HashMap;
use tera::TeraResult;
use crate::template::engine_adapter::JsonRegisterExt;
use super::value_to_string;
fn printf_default(v: &Value) -> String {
value_to_string(v).into_owned()
}
#[derive(Clone, Copy)]
struct PrintfSpec {
minus: bool,
plus: bool,
space: bool,
zero: bool,
hash: bool,
width: Option<usize>,
precision: Option<usize>,
verb: char,
}
fn pad(spec: &PrintfSpec, body: String, numeric_sign_prefix: Option<(&str, &str)>) -> String {
let (sign, prefix, core) = match numeric_sign_prefix {
Some((sign, prefix)) => (sign, prefix, body.as_str()),
None => ("", "", body.as_str()),
};
let assembled = format!("{}{}{}", sign, prefix, core);
let Some(width) = spec.width else {
return assembled;
};
let len = assembled.chars().count();
if len >= width {
return assembled;
}
let padding = width - len;
if spec.minus {
format!("{}{}", assembled, " ".repeat(padding))
} else if spec.zero && numeric_sign_prefix.is_some() {
format!("{}{}{}{}", sign, prefix, "0".repeat(padding), core)
} else if spec.zero {
format!("{}{}", "0".repeat(padding), assembled)
} else {
format!("{}{}", " ".repeat(padding), assembled)
}
}
fn pad_int(spec: &PrintfSpec, body: String, sign: &str, prefix: &str) -> String {
if spec.precision.is_some() && spec.zero {
let no_zero = PrintfSpec {
zero: false,
..*spec
};
pad(&no_zero, body, Some((sign, prefix)))
} else {
pad(spec, body, Some((sign, prefix)))
}
}
fn numeric_sign(negative: bool, plus: bool, space: bool) -> &'static str {
if negative {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
}
fn int_precision(digits: &str, precision: Option<usize>) -> String {
match precision {
Some(0) if digits == "0" => String::new(),
Some(p) if digits.len() < p => format!("{}{}", "0".repeat(p - digits.len()), digits),
_ => digits.to_string(),
}
}
fn go_exponent(s: &str, uppercase: bool) -> String {
let letter = if uppercase { 'E' } else { 'e' };
let Some(pos) = s.find(['e', 'E']) else {
return s.to_string();
};
let (mantissa, exp_part) = s.split_at(pos);
let exp_str = &exp_part[1..];
let (sign, digits) = match exp_str.strip_prefix('-') {
Some(rest) => ('-', rest),
None => ('+', exp_str.strip_prefix('+').unwrap_or(exp_str)),
};
let padded = if digits.len() < 2 {
format!("{:0>2}", digits)
} else {
digits.to_string()
};
format!("{}{}{}{}", mantissa, letter, sign, padded)
}
fn trim_fraction_zeros(s: &str) -> &str {
if !s.contains('.') {
return s;
}
let trimmed = s.trim_end_matches('0');
trimmed.strip_suffix('.').unwrap_or(trimmed)
}
fn format_g(mag: f64, precision: Option<usize>, uppercase: bool) -> String {
let sci = format!("{:e}", mag);
let exp: i32 = sci
.split(['e', 'E'])
.nth(1)
.and_then(|e| e.parse().ok())
.unwrap_or(0);
let eprec = precision.map(|p| p as i32).unwrap_or(6).max(1);
if exp < -4 || exp >= eprec {
let body = match precision {
Some(p) => format!("{:.*e}", p.saturating_sub(1), mag),
None => sci.clone(),
};
let normalized = go_exponent(&body, uppercase);
if precision.is_some()
&& let Some(epos) = normalized.find(['e', 'E'])
{
let (mantissa, exp_part) = normalized.split_at(epos);
return format!("{}{}", trim_fraction_zeros(mantissa), exp_part);
}
normalized
} else {
let body = match precision {
Some(p) => {
let frac = (p as i32 - (exp + 1)).max(0) as usize;
format!("{:.*}", frac, mag)
}
None => format!("{}", mag),
};
trim_fraction_zeros(&body).to_string()
}
}
const PRINTF_FIELD_MAX: usize = 100_000;
fn format_verb(spec: &PrintfSpec, value: Option<&Value>) -> Result<String, tera::Error> {
let val = || -> Result<&Value, tera::Error> {
value.ok_or_else(|| {
tera::Error::message(format!("printf: missing argument for %{}", spec.verb))
})
};
match spec.verb {
's' => {
let mut s = printf_default(val()?);
if let Some(prec) = spec.precision {
s = s.chars().take(prec).collect();
}
Ok(pad(spec, s, None))
}
'v' => Ok(pad(spec, printf_default(val()?), None)),
't' => {
let b = val()?
.as_bool()
.ok_or_else(|| tera::Error::message("printf: %t expects a boolean argument"))?;
Ok(pad(spec, b.to_string(), None))
}
'q' => {
let s = printf_default(val()?);
Ok(pad(spec, format!("{:?}", s), None))
}
'c' => {
let v = val()?;
let code = v
.as_u64()
.ok_or_else(|| tera::Error::message("printf: %c expects a non-negative integer"))?;
let ch = u32::try_from(code)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| {
tera::Error::message(format!("printf: %c: {} is not a valid code point", code))
})?;
Ok(pad(spec, ch.to_string(), None))
}
'd' => {
let n = val()?
.as_i64()
.ok_or_else(|| tera::Error::message("printf: %d expects an integer argument"))?;
let sign = numeric_sign(n < 0, spec.plus, spec.space);
Ok(pad_int(
spec,
int_precision(&n.unsigned_abs().to_string(), spec.precision),
sign,
"",
))
}
'b' | 'o' | 'x' | 'X' => {
let n = val()?.as_i64().ok_or_else(|| {
tera::Error::message(format!(
"printf: %{} expects an integer argument",
spec.verb
))
})?;
let mag = n.unsigned_abs();
let digits = match spec.verb {
'b' => format!("{:b}", mag),
'o' => format!("{:o}", mag),
'x' => format!("{:x}", mag),
'X' => format!("{:X}", mag),
_ => unreachable!(),
};
let body = int_precision(&digits, spec.precision);
let sign = numeric_sign(n < 0, spec.plus, spec.space);
let prefix = if spec.hash {
match spec.verb {
'b' => "0b",
'o' => "0",
'x' => "0x",
'X' => "0X",
_ => "",
}
} else {
""
};
Ok(pad_int(spec, body, sign, prefix))
}
'f' | 'e' | 'E' | 'g' | 'G' => {
let f = val()?.as_f64().ok_or_else(|| {
tera::Error::message(format!("printf: %{} expects a numeric argument", spec.verb))
})?;
let prec = spec.precision.unwrap_or(6);
let mag = f.abs();
let body = match spec.verb {
'f' => format!("{:.*}", prec, mag),
'e' | 'E' => go_exponent(&format!("{:.*e}", prec, mag), spec.verb == 'E'),
'g' | 'G' => format_g(mag, spec.precision, spec.verb == 'G'),
_ => unreachable!(),
};
let sign = numeric_sign(f.is_sign_negative() && f != 0.0, spec.plus, spec.space);
Ok(pad(spec, body, Some((sign, ""))))
}
other => Err(tera::Error::message(format!(
"printf: unsupported verb %{} (supported: s d v x X o b c q f e E g G t %%)",
other
))),
}
}
fn sprintf(format: &str, args: &[Value]) -> Result<String, tera::Error> {
let mut out = String::new();
let mut arg_idx = 0usize;
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
if chars.peek() == Some(&'%') {
chars.next();
out.push('%');
continue;
}
let mut spec = PrintfSpec {
minus: false,
plus: false,
space: false,
zero: false,
hash: false,
width: None,
precision: None,
verb: ' ',
};
while let Some(&f) = chars.peek() {
match f {
'-' => spec.minus = true,
'+' => spec.plus = true,
' ' => spec.space = true,
'0' => spec.zero = true,
'#' => spec.hash = true,
_ => break,
}
chars.next();
}
let mut width_digits = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
width_digits.push(d);
chars.next();
} else {
break;
}
}
if !width_digits.is_empty() {
let w = width_digits.parse::<usize>().unwrap_or(usize::MAX);
if w > PRINTF_FIELD_MAX {
return Err(tera::Error::message(format!(
"printf width {} exceeds maximum {}",
width_digits, PRINTF_FIELD_MAX
)));
}
spec.width = Some(w);
}
if chars.peek() == Some(&'.') {
chars.next();
let mut prec_digits = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
prec_digits.push(d);
chars.next();
} else {
break;
}
}
let p = if prec_digits.is_empty() {
0
} else {
prec_digits.parse::<usize>().unwrap_or(usize::MAX)
};
if p > PRINTF_FIELD_MAX {
return Err(tera::Error::message(format!(
"printf precision {} exceeds maximum {}",
prec_digits, PRINTF_FIELD_MAX
)));
}
spec.precision = Some(p);
}
let verb = chars.next().ok_or_else(|| {
tera::Error::message("printf: format string ends with a dangling '%'")
})?;
spec.verb = verb;
let value = args.get(arg_idx);
out.push_str(&format_verb(&spec, value)?);
arg_idx += 1;
}
Ok(out)
}
pub(super) fn register(tera: &mut tera::Tera) {
tera.register_json_function(
"printf",
|args: &HashMap<String, Value>| -> TeraResult<Value> {
let format = args
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::message("printf requires a `format` argument"))?;
let fmt_args: Vec<Value> = args
.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
Ok(Value::String(sprintf(format, &fmt_args)?))
},
);
tera.register_json_function(
"print",
|args: &HashMap<String, Value>| -> TeraResult<Value> {
let fmt_args: Vec<Value> = args
.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut out = String::new();
for (i, v) in fmt_args.iter().enumerate() {
if i > 0 {
let prev_str = fmt_args[i - 1].is_string();
let cur_str = v.is_string();
if !prev_str && !cur_str {
out.push(' ');
}
}
out.push_str(&printf_default(v));
}
Ok(Value::String(out))
},
);
tera.register_json_function(
"println",
|args: &HashMap<String, Value>| -> TeraResult<Value> {
let fmt_args: Vec<Value> = args
.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut joined = fmt_args
.iter()
.map(printf_default)
.collect::<Vec<_>>()
.join(" ");
joined.push('\n');
Ok(Value::String(joined))
},
);
}