defmodule HtmlQuery.Css do
# @related [test](/test/css_test.exs)
@moduledoc """
Constructs CSS selectors via Elixir data structures. See `selector/1` for details.
"""
@typedoc "See docs for `selector/1` for more details."
@type selector() :: binary() | atom() | keyword() | list()
@doc ~S"""
Accepts a string, atom, keyword list, or list, and returns a CSS string.
## String syntax
When given a string, returns the string. This is useful when you don't know if a selector is already a string.
```elixir
iex> HtmlQuery.Css.selector(".profile[test-role='new-members']")
".profile[test-role='new-members']"
```
## Atom syntax
When given an atom, converts the atom to a string, without converting underscores to dashes.
```elixir
iex> HtmlQuery.Css.selector(:p)
"p"
```
## Keyword list syntax
_The keyword list syntax is intentionally limited; complex selectors are more
easily written as strings._
The keyword list syntax makes it a bit eaiser to write simple selectors or selectors that use variables:
e.g.:
```elixir
HtmlQuery.Css.selector(test_role: "new-members")
HtmlQuery.Css.selector(id: some_variable)
```
Keys are expected to be atoms and will be dasherized (`foo_bar` -> `foo-bar`).
Values are expected to be strings or another keyword list.
A key/value pair will be converted to an attribute selector:
```elixir
iex> HtmlQuery.Css.selector(test_role: "new-members")
"[test-role='new-members']"
```
A keyword list will be converted to a list of attribute selectors:
```elixir
iex> HtmlQuery.Css.selector(class: "profile", test_role: "new-members")
"[class='profile'][test-role='new-members']"
```
(Note that the CSS selector `.profile` expands to `[class~='profile']` which is not equivalent to
`[class='profile']`. The keyword list syntax will not generate `~=` selectors so you should use a
string selector such as `".profile[test-role='new-members']"` instead if you want `~=` semantics. See the
[CSS spec](https://www.w3.org/TR/CSS21/selector.html#attribute-selectors) for details.)
When the value is a keyword list, the key is converted to an element selector:
```elixir
iex> HtmlQuery.Css.selector(p: [class: "profile", test_role: "new-members"])
"p[class='profile'][test-role='new-members']"
```
## List syntax
_The list syntax is intentionally limited; complex selectors are more
easily written as strings._
When the value is a list (vs a keyword list), atoms are converted to element selectors and
keyword lists are converted as described above.
```elixir
iex> HtmlQuery.Css.selector([[p: [class: "profile", test_role: "new-members"]], :div, [class: "tag"]])
"p[class='profile'][test-role='new-members'] div [class='tag']"
```
"""
@spec selector(selector()) :: binary()
def selector(input) when is_binary(input), do: input
def selector(input) when is_atom(input), do: input |> to_string() |> selector()
def selector(input) when is_list(input), do: reduce(input) |> Moar.String.squish()
defp reduce(input, result \\ "")
defp reduce([head | tail], result), do: result <> reduce(head) <> reduce(tail)
defp reduce({k, v}, result) when is_atom(k), do: reduce({k |> to_string() |> Moar.String.dasherize(), v}, result)
defp reduce({k, v}, result) when is_list(v), do: "#{result} #{k}#{reduce(v)}"
defp reduce({_k, false}, result), do: result
defp reduce({k, true}, result), do: "#{result}[#{k}]"
defp reduce({k, v}, result), do: "#{result}[#{k}='#{v}']"
defp reduce(term, result), do: "#{result} #{term} "
end