Current section
Files
Jump to
Current section
Files
lib/css_parser.ex
defmodule CssParser do
import CssParser.File
alias CssParser.Cache
@moduledoc """
Provides css parsing in Elixir.
CssParser is based on css.js (a lightweight, battle tested, fast, css parser in JavaScript)
and implemented for Elixir. More information can be found at https://github.com/jotform/css.js.
### Adding CssParser
To add CssParser to an application, add it to `deps` in the mix.exs file:
```elixir
defp deps do
[
{:css_parser, ">= 0.1.0"}
]
end
```
### Usage
You can use CssParser either on a command line or a module.
#### On command line
iex> CssParser.parse("h4, h3 {color: blue; font-size: 20px;}")
[
%{
"rules" => "color: blue; font-size: 20px;",
"selectors" => "h4, h3",
"type" => "rules"
}
]
You can also parse css from a file as follows:
iex> CssParser.parse("/path/to/css/file", source: :file)
#### In a module
CssParser can be `alias`ed or `import`ed in a module:
```elixir
defmodule MyMod do
import CssParser
def my_css_parser(css_string) do
# use the imported `parse` function
parse(css_string)
end
end
```
"""
@doc """
Parses a css string to produce selectors, rules/descriptors and types.
It first tries to remove css comments that might be in the css string.
### Options
* `source: :file` - when set specifies the source of the css to be a file in the given string.
### Examples
iex> CssParser.parse("h4, h3 {color: blue; font-size: 20px;}")
[
%{
"rules" => "color: blue; font-size: 20px;",
"selectors" => "h4, h3",
"type" => "rules"
}
]
You can also parse css from a file as follows:
iex> CssParser.parse("/path/to/css/file", source: :file)
In case the file doesn't exist it returns:
`[:error, "File /path/to/css/file not found."]`
"""
@css_regex ~r/(?<selectors>[\s\S]*?){(?<rules>[\s\S]*)/i
@comment_regx ~r/(\/*\*[\s\S]*?\*?\/*)|(\/\*.*?\*\/)/
@spec parse(String.t(), source: :file | :parent | :child) :: [term()] | [{:error, String.t()}]
def parse(csstring, opts \\ [])
def parse(csstring, _opts) when csstring in ["", nil], do: []
def parse(csstring, opts) do
# try getting data from cache else initiliaze it
hash_key = Cache.hash(csstring)
case Cache.get(hash_key) do
{:ok, data} ->
data
{:error, _} ->
drop_comments(csstring, opts)
|> Enum.map(&parse_css/1)
|> Enum.map(&parse_rules/1)
|> Cache.save(hash_key, returning: true)
end
end
defp drop_comments(css_string, opts) do
css_lines = String.split(css_string, "\n", trim: true)
to_parse =
Enum.reduce(css_lines, [], fn line, acc ->
string =
if Regex.match?(@comment_regx, line) do
String.split(line, @comment_regx, trim: true)
|> Enum.reject(&Regex.match?(~r/^[\s\S](?![\s\S]*\{)/, &1))
else
line
end
[string | acc]
end)
|> Enum.reverse()
|> Enum.join()
case Keyword.get(opts, :source, :parent) do
:parent -> String.split(to_parse, ~r/\s*\}\s*|\s*\}\s/, trim: true)
:child -> String.split(to_parse, ~r/\s*\}\s*|\}/, trim: true)
:file -> parse_from_file(to_parse)
end
end
defp parse_css(string) when not is_binary(string), do: string
defp parse_css(string) do
case Regex.named_captures(@css_regex, string) do
nil ->
string
%{"selectors" => selectors} = css ->
cond do
selectors =~ "@font-face" ->
{rules, css} = Map.pop(css, "rules")
Map.put(css, "selectors", String.trim(selectors))
|> Map.put("type", "font-face")
|> Map.put("descriptors", parse_fonts(rules))
selectors =~ "@media" ->
{rules, css} = Map.pop(css, "rules")
Map.put(css, "selectors", String.trim(selectors))
|> Map.put("type", "media")
|> Map.put("children", parse(rules, source: :child))
true ->
Map.put(css, "selectors", String.trim(selectors))
|> Map.put("type", "rules")
end
end
end
defp parse_rules(%{"rules" => rules} = css) do
parsed_rules =
String.trim(rules)
|> String.split("\n", trim: true)
|> Enum.join()
Map.put(css, "rules", parsed_rules)
end
defp parse_rules(css), do: css
defp parse_fonts(rules) do
String.split(rules, ";", trim: true)
|> Enum.map(&:string.trim/1)
|> Enum.map(&:re.split(&1, ":", [:trim]))
|> Enum.map(&map_font/1)
end
defp map_font([key, value] = _rule) do
Map.put(%{}, :string.trim(key), :string.trim(value))
end
@doc """
Converts a parsed css to binary
#### After running:
iex> parsed = CssParser.parse("h4, h3 {color: blue; font-size: 20px;}")
#### You can then run:
iex> CssParser.to_binary(parsed)
This reverts/converts the previous parsed css to binary.
#### The function is especially useful if you need to modify the parsed css structure and then get back a binary.
"""
def to_binary(parsed_css) do
Enum.reduce(parsed_css, [], fn %{"type" => type, "selectors" => s} = parsed, acc ->
case type do
"rules" ->
str = IO.iodata_to_binary([s, " {\n\t", parsed["rules"], "\n}", line_break()])
[str | acc]
"font-face" ->
descriptors = insert_font_face(parsed["descriptors"])
str = IO.iodata_to_binary([s, " {\n", descriptors, "\n}", line_break()])
[str | acc]
"media" ->
children = insert_media_children(parsed["children"])
str = IO.iodata_to_binary([s, " {\n", children, "\n}", line_break()])
[str | acc]
end
end)
|> Enum.reverse()
|> IO.iodata_to_binary()
end
defp insert_font_face(descriptors) do
Enum.map(descriptors, fn descriptor ->
key = :maps.keys(descriptor) |> hd
value = :maps.values(descriptor) |> hd
IO.iodata_to_binary(["\t", key, ": ", value, ";", "\n"])
end)
end
defp insert_media_children(rules) do
Enum.map(rules, fn %{"rules" => r, "selectors" => s} ->
IO.iodata_to_binary(["\t", s, " {\n\t\t", r, "\n\t}"])
end)
end
defp line_break() do
if :os.type() == {:win32, :nt} , do: "\r\n", else: "\n"
end
end