use std::collections::BTreeMap;
use super::Value;
use super::node::{Align, Pipe};
pub(crate) fn apply(value: &Value, pipe: &Pipe) -> Value {
match pipe {
Pipe::Uppercase => map_str(value, str::to_uppercase),
Pipe::Lowercase => map_str(value, lowercase),
Pipe::Length => Value::Str(length(value).to_string()),
Pipe::Reverse => reverse(value),
Pipe::First => nth_end(value, End::First),
Pipe::Last => nth_end(value, End::Last),
Pipe::Rest => drop_end(value, End::First),
Pipe::AllButLast => drop_end(value, End::Last),
Pipe::Pairs => pairs(value),
Pipe::Alpha => Value::Str(alpha(value)),
Pipe::Roman => Value::Str(roman(value)),
Pipe::Chomp => Value::Str(stringify(value).trim_end_matches(['\n', '\r']).to_string()),
Pipe::Nowrap => value.clone(),
Pipe::Block {
align,
width,
left,
right,
} => Value::Str(block(&stringify(value), *align, *width, left, right)),
}
}
pub(crate) fn stringify(value: &Value) -> String {
match value {
Value::Str(s) => s.clone(),
Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
Value::List(items) => items.iter().map(stringify).collect(),
Value::Map(_) => "true".to_string(),
}
}
fn map_str(value: &Value, f: impl Fn(&str) -> String) -> Value {
match value {
Value::Bool(_) | Value::Map(_) => value.clone(),
other => Value::Str(f(&stringify(other))),
}
}
fn lowercase(s: &str) -> String {
s.chars().flat_map(char::to_lowercase).collect()
}
fn length(value: &Value) -> usize {
match value {
Value::List(items) => items.len(),
Value::Map(map) => map.len(),
Value::Bool(_) => 0,
Value::Str(s) => s.chars().count(),
}
}
fn reverse(value: &Value) -> Value {
match value {
Value::List(items) => Value::List(items.iter().rev().cloned().collect()),
Value::Str(s) => Value::Str(s.chars().rev().collect()),
Value::Bool(_) | Value::Map(_) => value.clone(),
}
}
#[derive(Clone, Copy)]
enum End {
First,
Last,
}
fn nth_end(value: &Value, end: End) -> Value {
let Value::List(items) = value else {
return value.clone();
};
let picked = match end {
End::First => items.first(),
End::Last => items.last(),
};
picked.cloned().unwrap_or(Value::Str(String::new()))
}
fn drop_end(value: &Value, end: End) -> Value {
let Value::List(items) = value else {
return value.clone();
};
let kept: Vec<Value> = match end {
End::First => items.iter().skip(1).cloned().collect(),
End::Last => {
let take = items.len().saturating_sub(1);
items.iter().take(take).cloned().collect()
}
};
Value::List(kept)
}
fn pairs(value: &Value) -> Value {
match value {
Value::Map(map) => Value::List(
map.iter()
.map(|(key, val)| record(key.clone(), val.clone()))
.collect(),
),
Value::List(items) => Value::List(
items
.iter()
.enumerate()
.map(|(i, val)| record((i + 1).to_string(), val.clone()))
.collect(),
),
other => other.clone(),
}
}
fn record(key: String, value: Value) -> Value {
let mut map = BTreeMap::new();
map.insert("key".to_string(), Value::Str(key));
map.insert("value".to_string(), value);
Value::Map(map)
}
fn as_int(value: &Value) -> Option<i64> {
let text = stringify(value);
let digits = text.strip_prefix('-').unwrap_or(&text);
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
text.parse().ok()
}
fn alpha(value: &Value) -> String {
match as_int(value) {
Some(n) if n >= 0 => {
let offset = u8::try_from(n % 26).unwrap_or(0);
char::from(b'a' - 1 + offset).to_string()
}
_ => stringify(value),
}
}
fn roman(value: &Value) -> String {
let Some(mut n) = as_int(value).filter(|n| (0..=3999).contains(n)) else {
return stringify(value);
};
let mut out = String::new();
for (amount, glyph) in [
(1000, "m"),
(900, "cm"),
(500, "d"),
(400, "cd"),
(100, "c"),
(90, "xc"),
(50, "l"),
(40, "xl"),
(10, "x"),
(9, "ix"),
(5, "v"),
(4, "iv"),
(1, "i"),
] {
while n >= amount {
out.push_str(glyph);
n -= amount;
}
}
out
}
fn block(content: &str, align: Align, width: usize, left: &str, right: &str) -> String {
let len = content.chars().count();
let pad = width.saturating_sub(len);
let body = match align {
Align::Left => format!("{content}{}", " ".repeat(pad)),
Align::Right => format!("{}{content}", " ".repeat(pad)),
Align::Center => {
let lead = pad / 2;
format!("{}{content}{}", " ".repeat(lead), " ".repeat(pad - lead))
}
};
let body = if right.is_empty() {
body.trim_end_matches(' ').to_string()
} else {
body
};
format!("{left}{body}{right}")
}