Case Converter — C 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 C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* case-converter — pure, deterministic text-case transformations.
*
* Language: C (C11, standard library only)
* Source: CosmoDev polyglot showcase port of the Case Converter tool.
* Canonical: src/lib/case.ts (the live TypeScript) plus this
* tool's python.py / rust.rs reference ports.
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Split any input into word tokens and re-emit it in upper / lower / title /
* sentence / camel / pascal / snake / kebab / constant / slug case — same
* inputs produce the same outputs as the TypeScript original, including edge
* cases.
*
* The C standard library has no Unicode case tables (towupper is
* locale-dependent), so this port maps ASCII letters and passes every other
* byte through verbatim — the tool's token alphabet is [a-zA-Z0-9] anyway.
* The single-byte scans are safe for UTF-8 payload because every ASCII test
* below only ever matches single-byte characters; multi-byte sequences ride
* along untouched inside (or between) tokens.
*/
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/* --- Character classification (mirrors the JS regexes' ASCII classes) ---- */
static bool is_ascii_lower(unsigned char c) { return c >= 'a' && c <= 'z'; }
static bool is_ascii_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; }
static bool is_ascii_digit(unsigned char c) { return c >= '0' && c <= '9'; }
static bool is_ascii_alnum(unsigned char c)
{
return is_ascii_lower(c) || is_ascii_upper(c) || is_ascii_digit(c);
}
/* JavaScript's \w = [A-Za-z0-9_]. */
static bool is_word_char(unsigned char c) { return is_ascii_alnum(c) || c == '_'; }
/* --- Growable byte buffer -------------------------------------------------- */
typedef struct {
char *data;
size_t len;
size_t cap;
} strbuf;
static bool strbuf_reserve(strbuf *b, size_t extra)
{
size_t need = b->len + extra + 1; /* room for a NUL terminator */
if (need <= b->cap)
return true;
size_t cap = b->cap ? b->cap : 16;
while (cap < need)
cap *= 2;
char *p = realloc(b->data, cap);
if (p == NULL)
return false;
b->data = p;
b->cap = cap;
return true;
}
static bool strbuf_push(strbuf *b, char c)
{
if (!strbuf_reserve(b, 1))
return false;
b->data[b->len++] = c;
return true;
}
static bool strbuf_append(strbuf *b, const char *s, size_t n)
{
if (!strbuf_reserve(b, n))
return false;
memcpy(b->data + b->len, s, n);
b->len += n;
return true;
}
/* Hand the buffer's NUL-terminated contents to the caller (NULL on OOM). */
static char *strbuf_take(strbuf *b)
{
if (!strbuf_push(b, '\0'))
return NULL;
char *out = b->data;
b->data = NULL;
b->len = 0;
b->cap = 0;
return out;
}
/* --- Word list -------------------------------------------------------------- */
typedef struct {
char **items; /* each element is an owned, malloc'd NUL-terminated string */
size_t len;
} word_list;
static bool word_list_push(word_list *wl, char *owned)
{
char **p = realloc(wl->items, (wl->len + 1) * sizeof *p);
if (p == NULL) {
free(owned);
return false;
}
wl->items = p;
wl->items[wl->len++] = owned;
return true;
}
/* Release a list returned by words() — public because words() hands the
* caller ownership of the list and every token in it. */
void word_list_free(word_list *wl)
{
for (size_t i = 0; i < wl->len; i++)
free(wl->items[i]);
free(wl->items);
wl->items = NULL;
wl->len = 0;
}
/* Close the current token, if any, pushing it into the output list. */
static void flush_token(strbuf *cur, word_list *out)
{
if (cur->len == 0)
return;
char *tok = strbuf_take(cur);
if (tok != NULL)
word_list_push(out, tok);
}
/*
* Tokenize `s` into word units — one forward pass fusing the three steps of
* the TypeScript reference:
* 1. Break at every camelCase boundary (a lowercase letter or digit
* immediately followed by an uppercase letter).
* 2. Break on runs of whitespace / underscore / hyphen / dot.
* 3. Keep ASCII letters and digits; drop everything else and empty tokens.
*
* Returns a list whose caller becomes the owner; release with
* word_list_free(). On allocation failure `items` is NULL and `len` is 0.
*/
word_list words(const char *s)
{
word_list out = {NULL, 0};
strbuf cur = {NULL, 0, 0};
size_t n = strlen(s);
for (size_t i = 0; i < n; i++) {
unsigned char c = (unsigned char)s[i];
/* 1. camelCase boundary: the previous char was lowercase-or-digit
* and this one is uppercase — close the current token. */
if (i > 0) {
unsigned char prev = (unsigned char)s[i - 1];
if ((is_ascii_lower(prev) || is_ascii_digit(prev)) && is_ascii_upper(c))
flush_token(&cur, &out);
}
if (isspace(c) || c == '_' || c == '-' || c == '.') {
/* 2. delimiter — end the current token. */
flush_token(&cur, &out);
} else if (is_ascii_alnum(c)) {
/* 3a. keep ASCII letters and digits. */
if (!strbuf_push(&cur, (char)c)) {
word_list_free(&out);
free(cur.data);
return (word_list){NULL, 0};
}
}
/* 3b. anything else (punctuation, symbols) is silently dropped,
* matching the reference's `[^a-zA-Z0-9]` stripping. */
}
flush_token(&cur, &out);
free(cur.data);
return out;
}
/* --- Case mapping (ASCII; other bytes pass through verbatim) --------------- */
/* Map ASCII letters through `map`; copy every other byte unchanged. */
static char *map_ascii(const char *s, int (*map)(int))
{
size_t n = strlen(s);
char *out = malloc(n + 1);
if (out == NULL)
return NULL;
for (size_t i = 0; i < n; i++)
out[i] = (char)map((unsigned char)s[i]);
out[n] = '\0';
return out;
}
/* Upper-case every ASCII letter. */
char *upper(const char *s) { return map_ascii(s, toupper); }
/* Lower-case every ASCII letter. */
char *lower(const char *s) { return map_ascii(s, tolower); }
/* Capitalize the first character and lowercase the rest. */
char *cap(const char *s)
{
size_t n = strlen(s);
char *out = malloc(n + 1);
if (out == NULL)
return NULL;
if (n > 0) {
out[0] = (char)toupper((unsigned char)s[0]);
for (size_t i = 1; i < n; i++)
out[i] = (char)tolower((unsigned char)s[i]);
}
out[n] = '\0';
return out;
}
/*
* 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 the JavaScript \w\S* word-run regex.
* Characters between
* runs (spacing, punctuation) are preserved verbatim — only each run's first
* character is touched.
*/
char *title(const char *s)
{
size_t n = strlen(s);
char *out = malloc(n + 1);
if (out == NULL)
return NULL;
size_t i = 0, o = 0;
while (i < n) {
unsigned char c = (unsigned char)s[i];
if (is_word_char(c)) {
/* Consume the whole `\w\S*` run up to the next whitespace. */
size_t start = i;
i++;
while (i < n && !isspace((unsigned char)s[i]))
i++;
out[o++] = (char)toupper((unsigned char)s[start]);
for (size_t k = start + 1; k < i; k++)
out[o++] = (char)tolower((unsigned char)s[k]);
} else {
/* Not the start of a word-run: copy verbatim. */
out[o++] = (char)c;
i++;
}
}
out[o] = '\0';
return out;
}
/*
* Sentence case: lowercase + trim the whole string, then uppercase the first
* character. Empty (or all-whitespace) input yields empty output.
*/
char *sentence(const char *s)
{
char *t = map_ascii(s, tolower);
if (t == NULL)
return NULL;
size_t n = strlen(t);
size_t a = 0, b = n; /* [a, b) is the trimmed span */
while (a < b && isspace((unsigned char)t[a]))
a++;
while (b > a && isspace((unsigned char)t[b - 1]))
b--;
if (b > a)
t[a] = (char)toupper((unsigned char)t[a]);
memmove(t, t + a, b - a);
t[b - a] = '\0';
return t;
}
/* --- Token-based transforms -------------------------------------------------- */
/* Join the tokens with `sep` into one owned string (NULL on OOM). */
static char *join_words(const word_list *wl, char sep)
{
strbuf b = {NULL, 0, 0};
for (size_t i = 0; i < wl->len; i++) {
if (i > 0 && !strbuf_push(&b, sep)) {
free(b.data);
return NULL;
}
if (!strbuf_append(&b, wl->items[i], strlen(wl->items[i]))) {
free(b.data);
return NULL;
}
}
return strbuf_take(&b);
}
/* camelCase: the first word stays lowercase, every later word is capitalized. */
char *camel(const char *s)
{
word_list w = words(s);
strbuf b = {NULL, 0, 0};
for (size_t i = 0; i < w.len; i++) {
char *piece = (i == 0) ? map_ascii(w.items[i], tolower) : cap(w.items[i]);
if (piece == NULL || !strbuf_append(&b, piece, strlen(piece))) {
free(piece);
word_list_free(&w);
free(b.data);
return NULL;
}
free(piece);
}
word_list_free(&w);
return strbuf_take(&b);
}
/* PascalCase: every word capitalized and concatenated. */
char *pascal(const char *s)
{
word_list w = words(s);
strbuf b = {NULL, 0, 0};
for (size_t i = 0; i < w.len; i++) {
char *piece = cap(w.items[i]);
if (piece == NULL || !strbuf_append(&b, piece, strlen(piece))) {
free(piece);
word_list_free(&w);
free(b.data);
return NULL;
}
free(piece);
}
word_list_free(&w);
return strbuf_take(&b);
}
/* Join + re-case helpers: `snake`/`kebab`/`constant`/`slug` all join the
* tokens first, then map the joined result — exactly like the reference's
* "_".join(words).lower() chains. */
static char *joined_case(const char *s, char sep, int (*map)(int))
{
word_list w = words(s);
char *joined = join_words(&w, sep);
word_list_free(&w);
if (joined == NULL)
return NULL;
char *out = map_ascii(joined, map);
free(joined);
return out;
}
char *snake(const char *s) { return joined_case(s, '_', tolower); }
char *kebab(const char *s) { return joined_case(s, '-', tolower); }
char *constant(const char *s) { return joined_case(s, '_', toupper); }
char *slug(const char *s) { return joined_case(s, '-', tolower); }
/* --- Case-id dispatch -------------------------------------------------------- */
/* Identifier for one of the supported case transformations — the C
* equivalent of the TypeScript `CaseId` union + caseTransforms keys. */
typedef enum {
CASE_UPPER,
CASE_LOWER,
CASE_TITLE,
CASE_SENTENCE,
CASE_CAMEL,
CASE_PASCAL,
CASE_SNAKE,
CASE_KEBAB,
CASE_CONSTANT,
CASE_SLUG
} case_id;
/* The registry's key list, in the same order as the TS object. */
static const char *const CASE_ID_NAMES[] = {
"upper", "lower", "title", "sentence", "camel",
"pascal", "snake", "kebab", "constant", "slug",
};
#define CASE_ID_COUNT (sizeof CASE_ID_NAMES / sizeof CASE_ID_NAMES[0])
/* Lowercase string form, matching the TypeScript object keys. */
const char *case_id_name(case_id id)
{
if ((size_t)id < CASE_ID_COUNT)
return CASE_ID_NAMES[(size_t)id];
return NULL;
}
/* Parse a case id from its lowercase string form. */
bool case_id_from_name(const char *name, case_id *out)
{
for (size_t i = 0; i < CASE_ID_COUNT; i++) {
if (strcmp(name, CASE_ID_NAMES[i]) == 0) {
*out = (case_id)i;
return true;
}
}
return false;
}
/* Apply the case transform identified by `id` (caller frees the result). */
char *convert(case_id id, const char *s)
{
switch (id) {
case CASE_UPPER: return upper(s);
case CASE_LOWER: return lower(s);
case CASE_TITLE: return title(s);
case CASE_SENTENCE: return sentence(s);
case CASE_CAMEL: return camel(s);
case CASE_PASCAL: return pascal(s);
case CASE_SNAKE: return snake(s);
case CASE_KEBAB: return kebab(s);
case CASE_CONSTANT: return constant(s);
case CASE_SLUG: return slug(s);
}
return NULL; /* unreachable — every enum value is handled above */
}
/* String-keyed dispatch: NULL when `name` is not a known case id (and on
* allocation failure). The raising `convert` analogue is
* case_id_from_name + convert. */
char *try_convert(const char *name, const char *s)
{
case_id id;
if (!case_id_from_name(name, &id))
return NULL;
return convert(id, 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 →