Case Converter — Rust source
Convert text between UPPER, lower, Title, Sentence, camelCase, PascalCase, snake_case, kebab-case, CONSTANT_CASE and slug. Live, one-click copy.
This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! case-converter — Rust polyglot showcase port.
//!
//! Pure, deterministic text-case transformations: split any input into word
//! tokens and re-emit it in upper / lower / title / sentence / camel / pascal /
//! snake / kebab / constant / slug case.
//!
//! Ported from src/lib/case.ts (the canonical TypeScript that powers the live
//! Case Converter tool on dev.cosmolabs.org). Functionally equivalent — same
//! inputs produce the same outputs, including edge cases.
//!
//! This is display source — part of CosmoDev's polyglot tool pages (one tool,
//! six language references). stdlib only — no external crates — so the word
//! splitting is a small hand-rolled state machine instead of the `regex` crate.
/// Tokenize `s` into lowercase-friendly word units.
///
/// Mirrors the TypeScript reference exactly:
/// 1. Insert a break at every camelCase boundary (a lowercase letter or digit
/// immediately followed by an uppercase letter).
/// 2. Split on runs of whitespace / underscore / hyphen / dot.
/// 3. Strip any remaining non-alphanumeric characters and drop empty tokens.
///
/// The three steps are fused into a single forward pass, so delimiter runs and
/// stripped characters never materialize as intermediate strings.
pub fn words(s: &str) -> Vec<String> {
let chars: Vec<char> = s.chars().collect();
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
for (i, &c) in chars.iter().enumerate() {
// 1. camelCase boundary: the previous char was lowercase-or-digit and
// this one is uppercase — close the current token and start fresh.
// (A boundary char that is itself a delimiter is handled below.)
if i > 0 {
let prev = chars[i - 1];
if (prev.is_ascii_lowercase() || prev.is_ascii_digit())
&& c.is_ascii_uppercase()
{
flush(&mut cur, &mut out);
}
}
if c.is_whitespace() || c == '_' || c == '-' || c == '.' {
// 2. delimiter — end the current token.
flush(&mut cur, &mut out);
} else if c.is_ascii_alphanumeric() {
// 3a. keep ASCII letters and digits.
cur.push(c);
}
// 3b. anything else (punctuation, symbols) is silently dropped,
// matching the reference's `[^a-zA-Z0-9]` stripping.
}
flush(&mut cur, &mut out);
out
}
/// Close the current token, if any, pushing it into the output.
fn flush(cur: &mut String, out: &mut Vec<String>) {
if !cur.is_empty() {
out.push(std::mem::take(cur));
}
}
/// Capitalize the first character and lowercase the rest.
///
/// Uses `char::to_uppercase` / `to_lowercase` so the transform is
/// unicode-aware, matching JavaScript's `toUpperCase` / `toLowerCase`.
pub fn cap(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut out: String = first.to_uppercase().collect();
out.extend(chars.flat_map(|c| c.to_lowercase()));
out
}
}
}
/// Upper-case every character.
pub fn upper(s: &str) -> String {
s.to_uppercase()
}
/// Lower-case every character.
pub fn lower(s: &str) -> String {
s.to_lowercase()
}
/// Title case: capitalize the first character of each maximal word-run.
///
/// A "word-run" starts at an ASCII word character (`[A-Za-z0-9_]`) and runs to
/// the next whitespace, mirroring JavaScript's `/\w\S*/g`. Characters between
/// runs (spacing, punctuation) are preserved verbatim — only each run's first
/// character is touched.
pub fn title(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(chars.len());
let mut i = 0;
while i < chars.len() {
if is_word_char(chars[i]) {
// Consume the whole `\w\S*` run up to the next whitespace.
let start = i;
i += 1;
while i < chars.len() && !chars[i].is_whitespace() {
i += 1;
}
let run: String = chars[start..i].iter().collect();
out.push_str(&cap(&run));
} else {
// Not the start of a word-run: copy verbatim.
out.push(chars[i]);
i += 1;
}
}
out
}
/// Whether `c` is an ASCII word character `[A-Za-z0-9_]` (JavaScript's `\w`).
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
/// Sentence case: lowercase + trim the whole string, then uppercase the first
/// character. Empty input yields empty output.
pub fn sentence(s: &str) -> String {
let t: String = s.to_lowercase().trim().to_string();
let mut chars = t.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut out: String = first.to_uppercase().collect();
out.extend(chars);
out
}
}
}
/// camelCase: the first word stays lowercase, every later word is capitalized.
pub fn camel(s: &str) -> String {
let mut out = String::new();
for (i, w) in words(s).into_iter().enumerate() {
if i == 0 {
out.push_str(&w.to_lowercase());
} else {
out.push_str(&cap(&w));
}
}
out
}
/// PascalCase: every word capitalized and concatenated.
pub fn pascal(s: &str) -> String {
let mut out = String::new();
for w in words(s) {
out.push_str(&cap(&w));
}
out
}
pub fn snake(s: &str) -> String {
words(s).join("_").to_lowercase()
}
pub fn kebab(s: &str) -> String {
words(s).join("-").to_lowercase()
}
pub fn constant(s: &str) -> String {
words(s).join("_").to_uppercase()
}
pub fn slug(s: &str) -> String {
words(s).join("-").to_lowercase()
}
/// Identifier for one of the supported case transformations.
///
/// In Rust a `enum` plus [`convert`] is the idiomatic equivalent of the
/// TypeScript `caseTransforms` object + `CaseId` union: exhaustive, statically
/// dispatched, and impossible to get out of sync at runtime.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum CaseId {
Upper,
Lower,
Title,
Sentence,
Camel,
Pascal,
Snake,
Kebab,
Constant,
Slug,
}
impl CaseId {
/// Lowercase string form, matching the TypeScript object keys.
pub fn as_str(self) -> &'static str {
match self {
CaseId::Upper => "upper",
CaseId::Lower => "lower",
CaseId::Title => "title",
CaseId::Sentence => "sentence",
CaseId::Camel => "camel",
CaseId::Pascal => "pascal",
CaseId::Snake => "snake",
CaseId::Kebab => "kebab",
CaseId::Constant => "constant",
CaseId::Slug => "slug",
}
}
/// Parse a case id from its lowercase string form.
pub fn from_str(id: &str) -> Option<CaseId> {
match id {
"upper" => Some(CaseId::Upper),
"lower" => Some(CaseId::Lower),
"title" => Some(CaseId::Title),
"sentence" => Some(CaseId::Sentence),
"camel" => Some(CaseId::Camel),
"pascal" => Some(CaseId::Pascal),
"snake" => Some(CaseId::Snake),
"kebab" => Some(CaseId::Kebab),
"constant" => Some(CaseId::Constant),
"slug" => Some(CaseId::Slug),
_ => None,
}
}
}
/// Apply the case transform identified by `id`.
///
/// String-keyed dispatch is available via [`CaseId::from_str`]:
/// `CaseId::from_str(name).map(|id| convert(id, s))`.
pub fn convert(id: CaseId, s: &str) -> String {
match id {
CaseId::Upper => upper(s),
CaseId::Lower => lower(s),
CaseId::Title => title(s),
CaseId::Sentence => sentence(s),
CaseId::Camel => camel(s),
CaseId::Pascal => pascal(s),
CaseId::Snake => snake(s),
CaseId::Kebab => kebab(s),
CaseId::Constant => constant(s),
CaseId::Slug => slug(s),
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →