TabOnly

Case Converter

camelCase, snake_case, kebab-case and the rest — with acronyms and digits split the way you meant.

input
tokens
digit boundary
output

every style appears here as you type

Correct tokenisation is the product

Case conversion itself is a lookup table. What almost every incumbent gets wrong is splitting the identifier into words in the first place, because the obvious regex — split on separators, then insert a break between a lowercase letter and an uppercase one — never fires inside a run of capitals. Measured against exactly that rule on this machine (Node 24.12): XMLHttpRequest becomes XMLHttp·Request, one token short of correct; IOError stays a single token; parseHTML5Document becomes parse·HTML5Document, the digit run swallowing the acronym after it.

This page runs four rules, in a fixed order, each measured against the cases above and against the four identifiers a looser acronym rule breaks: OAuth2Token, IPv4Address, OAuthToken and getIDs. The acronym rule needs two or more capitals before the boundary, not one or more — with just one, OAuth2Token becomes O·Auth2Token, which is worse than the incumbent it is trying to fix. A digit before a capitalised word (HTML5Document, OAuth2Token) is always a boundary; a lowercase letter before a digit (user2FA) is not, by default, because the identical shape shows up in md5sum, sha256 and base64url, which do not want the split. There is no correct default for that one case, so it is a control next to the token list, not a guess baked into the page.

What stays wrong on purpose

OAuth, IPad and IPhone correctly stay one token — a single leading capital before a lowercase run is never a boundary here. The same shape means XAxis, IError, TCell, gRPCClient and iOSDevice also stay whole, which is usually not what was meant. No regex tells the two intents apart; only a lexicon of known acronyms would, and a lexicon this page does not maintain. The token list under the input is exactly this rule's output, so a wrong split is visible on the page rather than silently wrong in whatever gets pasted next.

Case conversion is also not always reversible. 'ß'.toUpperCase() gives 'SS', and lowercasing that back gives 'ss', not ß. Turkish and Azeri distinguish a dotted and dotless I that the ordinary, locale-invariant casing rules do not — running Turkish text through toUpperCase()/toLowerCase() without the Turkish locale changes its length. This page flags a line as lossy when its case change does not round-trip, and carries a Turkish-locale toggle for the one case that needs it. Nothing here ever leaves the browser tab (verify it yourself).

Do it without this tool

javascript · the four rules, exactly as this page runs them
// The four boundary rules, in the order they must run. {2,} in R2 is the whole
// trick — [A-Z]+ regresses OAuth2Token, IPv4Address, OAuthToken and getIDs into
// wrong splits that these four rules, in this order, get right.
const R2 = s => s.replace(/([A-Z]{2,})([A-Z][a-z])/g, '$1 $2'); // XMLHttp  -> XML Http
const R1 = s => s.replace(/([a-z])([A-Z])/g,          '$1 $2'); // userID   -> user ID
const R3 = s => s.replace(/([0-9])([A-Z][a-z])/g,     '$1 $2'); // HTML5Doc -> HTML5 Doc
const R4 = s => s.replace(/([a-z])([0-9])/g,          '$1 $2'); // user2FA  -> user 2FA (opt-in — md5sum, v2, base64url do not want this)

const tokenize = (s, { splitDigits = false } = {}) => {
  let out = R3(R1(R2(s)));
  if (splitDigits) out = R4(out);
  return out.split(/[\s_\-./]+/).filter(Boolean);
};

tokenize('XMLHttpRequest');       // ['XML', 'Http', 'Request']
tokenize('parseHTML5Document');   // ['parse', 'HTML5', 'Document']
tokenize('OAuth2Token');          // ['OAuth2', 'Token']  — {2,} keeps OAuth whole
python · the re equivalent
import re

R2 = lambda s: re.sub(r'([A-Z]{2,})([A-Z][a-z])', r'\1 \2', s)
R1 = lambda s: re.sub(r'([a-z])([A-Z])',          r'\1 \2', s)
R3 = lambda s: re.sub(r'([0-9])([A-Z][a-z])',     r'\1 \2', s)
R4 = lambda s: re.sub(r'([a-z])([0-9])',          r'\1 \2', s)

def tokenize(s, split_digits=False):
    out = R3(R1(R2(s)))
    if split_digits:
        out = R4(out)
    return [t for t in re.split(r'[\s_\-./]+', out) if t]

tokenize('XMLHttpRequest')  # ['XML', 'Http', 'Request']
shell · a bulk-rename one-liner, and where it falls short
# Bulk-rename camelCase identifiers to snake_case across a file — good enough for
# the common case, and exactly the naive rule this page's tokeniser improves on
# (it will still mis-split an XMLHttpRequest-shaped identifier; check the diff).
# Portable across GNU and BSD/macOS sed — verified both, no labels or loops needed.
sed -E 's/([a-z0-9])([A-Z])/\1_\2/g' file.js | tr '[:upper:]' '[:lower:]'

# The same, restricted to identifiers only (skip strings/comments) needs a real
# parser — this one-liner is for a quick pass on a small file, not a codebase.
reference · naming conventions by language
# Naming conventions by language/style guide, for the row this page is naming:

Python (PEP 8)        snake_case functions/vars, PascalCase classes, UPPER_SNAKE constants
Rust (API guidelines)  snake_case fns/vars/modules, PascalCase types, SCREAMING_SNAKE_CASE consts/statics
Go                     camelCase unexported, PascalCase exported (case controls visibility)
JavaScript/TypeScript  camelCase vars/functions, PascalCase classes/types, UPPER_SNAKE for true constants
Java (Google style)    camelCase methods/fields, PascalCase classes, CONSTANT_CASE static finals
C++ (Google style)     PascalCase types/functions, snake_case locals, kConstantCase constants
CSS/HTML               kebab-case for classes, ids and custom properties
URL paths               kebab-case or lowercase, rarely camelCase

The specs, which one says what

PEP 8 is the reference for Python's snake_case functions and variables, PascalCase classes and UPPER_SNAKE_CASE constants. The Google style guides (C++, Java) and the Rust API guidelines cover the same ground for their languages, including the constant-naming split this page's FAQ answers. Unicode Standard Annex #21 (Case Mappings) is the spec behind the non-reversible casing rows above — the special-casing tables for ß, ligatures, and the Turkish I pair.

Renaming is the workflow: JSON string escape/unescape and URL encode/decode for where a renamed identifier often ends up next, and text & JSON diff for checking the rename itself, line by line.

FAQ

Why does XMLHttpRequest become xmlhttp_request on most converters?

Most converters split only on a lowercase-to-uppercase boundary, which never fires inside a run of capitals — so an acronym and the word after it stay glued together. This page adds a second, ordered rule for a run of two or more capitals before a capitalised word, which is what actually separates the acronym first.

Should HTML5Document split into HTML5 and Document?

Yes — a digit before a capitalised word is an unconditional boundary here, so HTML5Document and OAuth2Token both split at the digit regardless of any other setting.

Why does XAxis stay one word when XMLHttp splits?

The acronym rule needs a second capitalised word after the capital run to fire; XMLHttpRequest has one and XAxis does not. The same protection that correctly keeps OAuth whole keeps XAxis whole too, whether or not that was wanted — no regex tells the two cases apart.

Why did my identifier change length when I uppercased it?

German's ß and the ligature have no matching single-character form in the other case, so uppercasing then lowercasing them does not return the original character. This page flags the line rather than silently returning something that will not round-trip.

What is the Turkish İ/ı problem, and when does it actually bite?

Turkish and Azeri distinguish a dotted and dotless I that the ordinary casing rules do not, so running Turkish text through the invariant toUpperCase()/toLowerCase() can change its length. The tr-locale toggle applies the correct tables instead.

What is SCREAMING_SNAKE_CASE called in each language?

PEP 8 calls it the constants convention without a separate nickname, Rust's API guidelines name it SCREAMING_SNAKE_CASE explicitly, and the Google style guides call it CONSTANT_CASE (C++) or the same SCREAMING_SNAKE_CASE in Java.

related tools