281 words, 2 min read

When normalizing user input like titles or descriptions, it’s common to lowercase everything and capitalize the first word. But what if you want to preserve specific acronyms or names like API, CSS, or Elixir?

In this post, we’ll build a small Elixir utility to:

  • Lowercase and normalize input
  • Capitalize the first word of each sentence
  • Preserve casing for special words like API, HTML, MCP, etc.

Example

input = "i built this using elixir, html and css. then deployed the api to phoenix."
TitleCaser.smart_case(input)
# => "I built this using Elixir, HTML and CSS. Then deployed the API to Phoenix."

Implementation

defmodule TitleCaser do
@special_words ~w(
I CSS MCP API HTTP HTML XML Elixir Phoenix Go Golang JavaScript TypeScript JSON
)
def smart_case(text) do
text
|> split_sentences()
|> Enum.map(&process_sentence/1)
|> Enum.join(" ")
end
defp split_sentences(text) do
Regex.split(~r/(?<=[.!?])\s+/, text)
end
defp process_sentence(sentence) do
sentence
|> String.downcase()
|> String.trim()
|> String.split(~r/\s+/, trim: true)
|> case do
[] -> ""
[first | rest] ->
[String.capitalize(first) | rest]
|> Enum.map(&restore_special_word/1)
|> Enum.join(" ")
end
end
defp restore_special_word(word) do
{base, punctuation} = split_word_and_punctuation(word)
restored =
Enum.find(@special_words, fn w ->
String.downcase(w) == String.downcase(base)
end) || base
restored <> punctuation
end
defp split_word_and_punctuation(word) do
case Regex.run(~r/^(.+?)([.,!?;:]*)$/, word, capture: :all_but_first) do
[base, punctuation] -> {base, punctuation}
_ -> {word, ""}
end
end
end

Tests

Use ExUnit to verify correct behavior:

test "preserves casing for acronyms" do
input = "mcp and json are used in the api. i wrote it in go."
expected = "MCP and JSON are used in the API. I wrote it in Go."
assert TitleCaser.smart_case(input) == expected
end

Tip: make special words configurable

To make this reusable, consider passing @special_words as a config or module attribute override.