#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UrlEncodeKind {
Query,
Wiki,
Path,
}
impl UrlEncodeKind {
pub(crate) fn parse(arg: &str) -> Self {
match arg.trim().to_ascii_uppercase().as_str() {
"WIKI" => UrlEncodeKind::Wiki,
"PATH" => UrlEncodeKind::Path,
_ => UrlEncodeKind::Query,
}
}
}
pub(crate) fn lc(s: &str) -> String {
s.to_lowercase()
}
pub(crate) fn uc(s: &str) -> String {
s.to_uppercase()
}
pub(crate) fn lcfirst(s: &str) -> String {
map_first(s, char::to_lowercase)
}
pub(crate) fn ucfirst(s: &str) -> String {
map_first(s, char::to_uppercase)
}
fn map_first<I: Iterator<Item = char>>(s: &str, f: impl FnOnce(char) -> I) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut out = String::with_capacity(s.len());
out.extend(f(first));
out.push_str(chars.as_str());
out
}
}
}
pub(crate) fn padleft(s: &str, width: usize, pad: &str) -> String {
match pad_prefix(s, width, pad) {
None => s.to_owned(),
Some(prefix) => format!("{prefix}{s}"),
}
}
pub(crate) fn padright(s: &str, width: usize, pad: &str) -> String {
match pad_prefix(s, width, pad) {
None => s.to_owned(),
Some(suffix) => format!("{s}{suffix}"),
}
}
fn pad_prefix(s: &str, width: usize, pad: &str) -> Option<String> {
let len = s.chars().count();
if len >= width || pad.is_empty() {
return None;
}
let needed = width - len;
Some(pad.chars().cycle().take(needed).collect())
}
pub(crate) fn urlencode(s: &str, kind: UrlEncodeKind) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
' ' => match kind {
UrlEncodeKind::Query => out.push('+'),
UrlEncodeKind::Wiki => out.push('_'),
UrlEncodeKind::Path => out.push_str("%20"),
},
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' => out.push(c),
other => percent_encode(&mut out, other),
}
}
out
}
pub(crate) fn anchorencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
' ' | '_' => out.push('_'),
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '.' | ':' => out.push(c),
other => percent_encode(&mut out, other),
}
}
out
}
fn percent_encode(out: &mut String, c: char) {
let mut buf = [0u8; 4];
for byte in c.encode_utf8(&mut buf).bytes() {
out.push('%');
out.push(hex_digit(byte >> 4));
out.push(hex_digit(byte & 0xf));
}
}
fn hex_digit(nibble: u8) -> char {
match nibble {
0..=9 => (b'0' + nibble) as char,
_ => (b'A' + nibble - 10) as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_functions_cover_ascii_and_unicode() {
assert_eq!(lc("HeLLo Мир"), "hello мир");
assert_eq!(uc("HeLLo Мир"), "HELLO МИР");
assert_eq!(lcfirst("Hello"), "hello");
assert_eq!(ucfirst("hello"), "Hello");
assert_eq!(ucfirst("привет"), "Привет");
assert_eq!(lcfirst(""), "");
assert_eq!(ucfirst(""), "");
assert_eq!(ucfirst("ßtraße"), "SStraße");
}
#[test]
fn padleft_and_padright_pad_truncate_and_no_op() {
assert_eq!(padleft("7", 3, "0"), "007");
assert_eq!(padright("7", 3, "0"), "700");
assert_eq!(padleft("xyz", 3, "0"), "xyz", "already wide enough");
assert_eq!(padleft("xyz", 2, "0"), "xyz", "narrower than string");
assert_eq!(padright("xyz", 2, "0"), "xyz", "padright no-op path");
assert_eq!(padright("x", 5, ""), "x", "padright empty pad is a no-op");
assert_eq!(padleft("x", 5, "ab"), "ababx", "multi-char pad repeats");
assert_eq!(padright("x", 4, "ab"), "xaba", "final pad truncated");
assert_eq!(padleft("x", 5, ""), "x", "empty pad is a no-op, not a hang");
assert_eq!(padleft("мир", 5, "0"), "00мир", "width counts characters");
}
#[test]
fn urlencode_handles_each_space_flavor_and_reserved_chars() {
assert_eq!(urlencode("a b&c", UrlEncodeKind::Query), "a+b%26c");
assert_eq!(urlencode("a b&c", UrlEncodeKind::Wiki), "a_b%26c");
assert_eq!(urlencode("a b&c", UrlEncodeKind::Path), "a%20b%26c");
assert_eq!(urlencode("keep-_.", UrlEncodeKind::Query), "keep-_.");
assert_eq!(urlencode("é", UrlEncodeKind::Query), "%C3%A9");
}
#[test]
fn url_encode_kind_parses_case_insensitively_with_query_default() {
assert_eq!(UrlEncodeKind::parse("wiki"), UrlEncodeKind::Wiki);
assert_eq!(UrlEncodeKind::parse(" PATH "), UrlEncodeKind::Path);
assert_eq!(UrlEncodeKind::parse("query"), UrlEncodeKind::Query);
assert_eq!(UrlEncodeKind::parse("nonsense"), UrlEncodeKind::Query);
}
#[test]
fn anchorencode_keeps_anchor_safe_chars() {
assert_eq!(anchorencode("Section one"), "Section_one");
assert_eq!(anchorencode("a_b"), "a_b");
assert_eq!(anchorencode("Note:1.2"), "Note:1.2");
assert_eq!(anchorencode("x&y"), "x%26y");
}
}