Norma
Normalize URLs to the format you need.
Installation
Add Norma to your list of dependencies in mix.exs. Tracking the latest release is
recommended:
def deps do
[
{:norma, ">= 0.0.0"}
]
end
If you prefer to pin the minor line:
{:norma, "~> 1.9"}
Documentation is on HexDocs.
Note on compatibility
Norma requires Elixir ~> 1.11 and is tested against Elixir 1.13 through 1.19.
It leans heavily on the standard library's URI module, whose parsing behavior has shifted
across Elixir releases. If you hit a surprising result, the module's
history is usually
the fastest explanation.
Usage
Two public functions. Both take a URL string and an optional map of options.
| Function | Returns |
|---|---|
Norma.normalize(url, opts \\ %{}) | String.t(); input it cannot parse as a URL is returned unchanged |
Norma.normalize_if_valid(url, opts \\ %{}) | {:ok, String.t()} or {:error, "Not an URL."} |
iex> Norma.normalize("example.com")
"http://example.com"
iex> Norma.normalize_if_valid("example.com")
{:ok, "http://example.com"}
iex> Norma.normalize_if_valid("example")
{:error, "Not an URL."}
Use normalize_if_valid/2 when the input is untrusted — user submissions, scraped text,
model output. Use normalize/2 only when the value is already known to be a URL.
Options are a map. Every key defaults to false, and any subset may be combined.
| Option | Effect |
|---|---|
remove_scheme: true | Drops http:// / https:// from the output |
remove_fragment: true | Drops everything from # onward |
remove_www: true | Drops a leading www. from the host |
downcase_host: true | Lowercases the host only; the path keeps its case |
add_trailing_slash: true | Appends / to the path when it does not look like a file |
force_root_path: true | Replaces the path with / |
iex> Norma.normalize("https://example.com", %{remove_scheme: true})
"example.com"
iex> Norma.normalize("https://example.com#faqs", %{remove_fragment: true})
"https://example.com"
iex> Norma.normalize("https://www.example.com", %{remove_www: true})
"https://example.com"
iex> Norma.normalize("https://EXAMPLE.COM/FAQS", %{downcase_host: true})
"https://example.com/FAQS"
iex> Norma.normalize("https://example.com/docs", %{add_trailing_slash: true})
"https://example.com/docs/"
iex> Norma.normalize("https://example.com/docs", %{force_root_path: true})
"https://example.com/"
iex> Norma.normalize("//www.example.com:1337/test#test",
...> %{remove_fragment: true, force_root_path: true, remove_www: true})
"http://example.com:1337/"
Behavior worth knowing before you rely on it
- A scheme-less input is assumed to be
http. Norma never upgrades tohttps. - Ports
80,443,8080and21are stripped from the output regardless of scheme. force_root_pathreplaces the path only; an existing query and fragment survive it.- Query parameters are sorted alphabetically and duplicate keys are collapsed, so
?tag=a&tag=bdoes not round-trip. add_trailing_slashreads any.in the path as a file extension, so/v1.2/docsis left without a slash.
These are the behaviors of the 1.x line, pinned by the test suite as-is. They are queued to
change in 2.0.
With Ecto
def creation_changeset(params) do
norma_options = %{
remove_www: true,
force_root_path: true,
remove_fragment: true
}
%MyEntity{}
|> cast(params, @fields)
|> put_change(:url, Norma.normalize(params.url, norma_options))
end
Contributing
Adding options
- Add support for the option in
/lib/norma/normalizer.ex. Prefer pattern matching and guards overifs andcases. - Add a test in
/test/norma_test.exs. - Add documentation to the
README. - Send a PR 🎉
Maintainers
A Mazing project (Studio Closed!)
Originally sponsored by Mazing Studio.