use crate::{
error::{EvalError, EvalResult, InterpreterError},
eval::modules::arg_str,
value::{Value, shared_list},
};
pub fn has_function(name: &str) -> bool {
matches!(name, "dedent" | "indent" | "wrap" | "fill" | "shorten")
}
pub fn call(func: &str, args: &[Value]) -> EvalResult {
match func {
"dedent" => {
let s = arg_str(func, args, 0)?;
Ok(Value::String(dedent(s).into()))
}
"indent" => {
let s = arg_str(func, args, 0)?;
let prefix = arg_str(func, args, 1)?;
let result = s
.lines()
.map(
|line| {
if line.is_empty() { line.to_string() } else { format!("{prefix}{line}") }
},
)
.collect::<Vec<_>>()
.join("\n");
let mut out = result;
if s.ends_with('\n') && !out.ends_with('\n') {
out.push('\n');
}
Ok(Value::String(out.into()))
}
"fill" => {
let s = arg_str(func, args, 0)?;
let width = opt_width(args, 1).unwrap_or(70);
Ok(Value::String(fill(s, width).into()))
}
"wrap" => {
let s = arg_str(func, args, 0)?;
let width = opt_width(args, 1).unwrap_or(70);
let lines = wrap(s, width).into_iter().map(|w| Value::String(w.into())).collect();
Ok(Value::List(shared_list(lines)))
}
"shorten" => {
let s = arg_str(func, args, 0)?;
let width = opt_width(args, 1).ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"shorten() requires a width argument".into(),
))
})?;
let placeholder = args
.get(2)
.and_then(|v| match v {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.unwrap_or_else(|| " [...]".to_string());
Ok(Value::String(shorten(s, width, &placeholder).into()))
}
_ => Err(InterpreterError::AttributeError(format!(
"module 'textwrap' has no attribute '{func}'"
))
.into()),
}
}
fn dedent(s: &str) -> String {
let non_empty: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if non_empty.is_empty() {
return s.to_string();
}
let common = non_empty
.iter()
.map(|line| line.chars().take_while(|c| c.is_whitespace() && *c != '\n').count())
.min()
.unwrap_or(0);
let mut out = String::new();
for (i, line) in s.lines().enumerate() {
if i > 0 {
out.push('\n');
}
if line.trim().is_empty() {
out.push_str(line);
} else {
let mut chars = line.chars();
for _ in 0..common {
chars.next();
}
out.push_str(chars.as_str());
}
}
if s.ends_with('\n') && !out.ends_with('\n') {
out.push('\n');
}
out
}
fn wrap(text: &str, width: usize) -> Vec<String> {
if width == 0 {
return vec![text.to_string()];
}
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if current.is_empty() {
if word.len() > width {
lines.push(word.to_string());
} else {
current.push_str(word);
}
} else if current.len() + 1 + word.len() <= width {
current.push(' ');
current.push_str(word);
} else {
lines.push(current.clone());
current.clear();
if word.len() > width {
lines.push(word.to_string());
} else {
current.push_str(word);
}
}
}
if !current.is_empty() {
lines.push(current);
}
lines
}
fn fill(text: &str, width: usize) -> String {
wrap(text, width).join("\n")
}
fn shorten(text: &str, width: usize, placeholder: &str) -> String {
let collapsed = text.split_whitespace().collect::<Vec<_>>().join(" ");
if collapsed.len() <= width {
return collapsed;
}
if placeholder.len() >= width {
return placeholder.to_string();
}
let available = width.saturating_sub(placeholder.len());
let cap = available.min(collapsed.len());
let truncated = collapsed[..cap]
.rfind(' ')
.map_or_else(|| collapsed[..cap].to_string(), |idx| collapsed[..idx].to_string());
format!("{truncated}{placeholder}")
}
fn opt_width(args: &[Value], index: usize) -> Option<usize> {
match args.get(index)? {
Value::Int(n) => usize::try_from(*n).ok(),
Value::Bool(b) => Some(usize::from(*b)),
_ => None,
}
}
pub struct TextwrapModule;
#[async_trait::async_trait]
impl crate::eval::modules::Module for TextwrapModule {
fn name(&self) -> &'static str {
"textwrap"
}
fn has_function(&self, name: &str) -> bool {
has_function(name)
}
async fn call(
&self,
_state: &mut crate::state::InterpreterState,
func: &str,
args: &[Value],
_kwargs: &indexmap::IndexMap<String, Value>,
_tools: &crate::tools::Tools,
) -> EvalResult {
call(func, args)
}
}