use std::path::{Path, PathBuf};
pub fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
if !components.is_empty() {
components.pop();
}
}
std::path::Component::CurDir => {}
other => components.push(other),
}
}
components.iter().collect()
}
pub fn find_project_root(start: &Path, markers: &[&str]) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
for marker in markers {
if current.join(marker).exists() {
return Some(current);
}
}
if !current.pop() {
return None;
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FormatConfig {
pub max_width: Option<usize>,
pub indent: String,
pub trim_trailing: bool,
pub normalize_newlines: bool,
}
impl FormatConfig {
pub fn new() -> Self {
Self {
max_width: Some(80),
indent: " ".to_string(),
trim_trailing: true,
normalize_newlines: true,
}
}
pub fn with_max_width(mut self, width: usize) -> Self {
self.max_width = Some(width);
self
}
pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
self.indent = indent.into();
self
}
}
pub fn format_string(input: &str, config: &FormatConfig) -> String {
let mut output = input.to_string();
if config.normalize_newlines {
output = output.replace("\r\n", "\n").replace('\r', "\n");
}
if config.trim_trailing {
output = output
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n");
}
if let Some(max_width) = config.max_width {
output = wrap_lines(&output, max_width);
}
output
}
fn wrap_lines(input: &str, max_width: usize) -> String {
let mut result = Vec::new();
for line in input.lines() {
if line.len() <= max_width {
result.push(line.to_string());
} else {
let mut current_line = String::new();
for word in line.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= max_width {
current_line.push(' ');
current_line.push_str(word);
} else {
result.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
result.push(current_line);
}
}
}
result.join("\n")
}
pub fn parse_duration(input: &str) -> Result<u64, String> {
let input = input.trim();
if input.is_empty() {
return Err("empty duration string".to_string());
}
let (num_str, unit) = input.split_at(input.len() - 1);
let num: u64 = num_str
.parse()
.map_err(|_| format!("invalid number: {}", num_str))?;
let multiplier = match unit {
"s" => 1,
"m" => 60,
"h" => 3600,
"d" => 86400,
_ => return Err(format!("unknown unit: {}", unit)),
};
Ok(num * multiplier)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(normalize_path("/foo/bar/../baz"), PathBuf::from("/foo/baz"));
assert_eq!(normalize_path("/foo/./bar"), PathBuf::from("/foo/bar"));
}
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("5m"), Ok(300));
assert_eq!(parse_duration("2h"), Ok(7200));
assert_eq!(parse_duration("1d"), Ok(86400));
assert!(parse_duration("").is_err());
assert!(parse_duration("5x").is_err());
}
#[test]
fn test_format_config() {
let config = FormatConfig::new().with_max_width(100).with_indent(" ");
assert_eq!(config.max_width, Some(100));
assert_eq!(config.indent, " ");
}
#[test]
fn test_format_string_trailing_whitespace() {
let config = FormatConfig::new();
let input = "hello \nworld ";
let output = format_string(input, &config);
assert!(!output.lines().any(|l| l.ends_with(' ')));
}
}