pub(crate) fn slug(text: &str) -> String {
let mut out = String::new();
for c in text.trim().chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
} else if (c == ' ' || c == '-' || c == '_') && !out.ends_with('-') && !out.is_empty() {
out.push('-');
}
}
out.trim_end_matches('-').to_string()
}
pub(super) fn parse_flag(word: &str) -> Result<String, String> {
if word.is_empty()
|| !word
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
{
return Err(format!(
"'{}' is not a variable name (lowercase letters, digits, `_`, `-`)",
word
));
}
Ok(word.to_string())
}
pub(super) fn parse_int(word: &str) -> Result<i32, String> {
word.parse::<i32>()
.map_err(|_| format!("'{}' is not an integer", word))
}
pub(super) fn split_top_level(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'[' | '{' => depth += 1,
']' | '}' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&s[start..]);
parts
}
pub(super) fn unquote(s: &str) -> &str {
let s = s.trim();
s.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.unwrap_or(s)
}
pub(crate) fn wrap_text(text: &str, columns: usize) -> String {
let mut lines: Vec<String> = Vec::new();
for source_line in text.split('\n') {
let mut cur = String::new();
for word in source_line.split_whitespace() {
if cur.is_empty() {
cur = word.to_string();
} else if cur.chars().count() + 1 + word.chars().count() <= columns {
cur.push(' ');
cur.push_str(word);
} else {
lines.push(std::mem::take(&mut cur));
cur = word.to_string();
}
}
lines.push(cur);
}
lines.join("\n")
}
pub(super) const AUDIO_EXTENSIONS: [&str; 4] = ["ogg", "wav", "mp3", "flac"];
pub(super) const IMAGE_EXTENSIONS: [&str; 3] = ["png", "jpg", "jpeg"];
pub(super) fn file_extension(target: &str) -> String {
std::path::Path::new(target)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.unwrap_or_default()
}