Current section

Files

Jump to
norma README.md
Raw

README.md

# Norma
[![Hex.pm](https://img.shields.io/hexpm/v/norma.svg?style=flat-square)](https://hex.pm/packages/norma)
[![Hexdocs](https://img.shields.io/badge/hex-docs-purple.svg?style=flat-square)](https://hexdocs.pm/norma)
[![Downloads](https://img.shields.io/hexpm/dt/norma.svg?style=flat-square)](https://hex.pm/packages/norma)
[![License](https://img.shields.io/hexpm/l/norma.svg?style=flat-square)](https://github.com/mazingstudio/norma/blob/master/LICENSE)
Normalize URLs to the format you need.
## Installation
Add `Norma` to your list of dependencies in `mix.exs`. Tracking the latest release is
recommended:
```elixir
def deps do
[
{:norma, ">= 0.0.0"}
]
end
```
If you prefer to pin the minor line:
```elixir
{:norma, "~> 1.9"}
```
Documentation is on [HexDocs](https://hexdocs.pm/norma).
### 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](https://github.com/elixir-lang/elixir/commits/main/lib/elixir/lib/uri.ex) 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."}` |
```elixir
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 `/` |
```elixir
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 to `https`.
- Ports `80`, `443`, `8080` and `21` are stripped from the output regardless of scheme.
- `force_root_path` replaces the path only; an existing query and fragment survive it.
- Query parameters are sorted alphabetically and duplicate keys are collapsed, so
`?tag=a&tag=b` does not round-trip.
- `add_trailing_slash` reads any `.` in the path as a file extension, so `/v1.2/docs` is 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
```elixir
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
1. Add support for the option in `/lib/norma/normalizer.ex`. Prefer pattern matching and
guards over `if`s and `case`s.
2. Add a test in `/test/norma_test.exs`.
3. Add documentation to the `README`.
4. Send a PR 🎉
### Maintainers
- [Zura Guerra](https://github.com/ZuraGuerra)
---
## A Mazing project (Studio Closed!)
Originally sponsored by _Mazing Studio_.