Skip to content

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 →