Current section

Files

Jump to
premailex lib premailex css_parser.ex
Raw

lib/premailex/css_parser.ex

defmodule Premailex.CSSParser do
@moduledoc """
Module that handles CSS parsing with naive Regular Expression.
"""
@css_selector_rules ~r/([\s\S]*?){([\s\S]*?)}/mi
@non_id_attributes_and_pseudo_classes ~r/
(\.[\w]+) # classes
|
\[(\w+) # attributes
|
(\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix
@elements_and_pseudo_elements ~r/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before
|first-letter|first-line
|selection
)
)/ix
@doc """
Parses a CSS string into a map.
## Examples
iex> Premailex.CSSParser.parse("body { background-color: #fff !important; color: red; }")
[%{rules: [%{directive: "background-color", value: "#fff !important", important?: true},
%{directive: "color", value: "red", important?: false}],
selector: "body",
specificity: 1}]
"""
def parse(""), do: []
def parse(css) do
@css_selector_rules
|> Regex.scan(css)
|> Enum.map(&parse_selectors_rules(&1))
|> Enum.reduce([], &Enum.concat(&2, &1))
end
defp parse_selectors_rules([_, selector, rules]) do
selector
|> String.split(",")
|> Enum.map(&parse_selector_rules(&1, rules))
end
defp parse_selector_rules(selector, rules) do
%{selector: String.trim(selector), rules: parse_rules(rules), specificity: calculate_specificity(selector)}
end
@doc """
Parses a CSS rules string into a map.
## Examples
iex> Premailex.CSSParser.parse_rules("background-color: #fff; color: red;")
[%{directive: "background-color", value: "#fff", important?: false},
%{directive: "color", value: "red", important?: false}]
"""
def parse_rules(rules) do
rules
|> String.split(";")
|> Enum.map(&parse_rule(&1))
|> Enum.filter(&!is_nil(&1))
end
defp parse_rule(rule) when is_binary(rule) do
rule
|> String.trim()
|> String.split(":")
|> parse_rule
end
defp parse_rule([directive, value]) do
%{directive: String.trim(directive),
value: String.trim(value),
important?: String.contains?(value, "!important")}
end
defp parse_rule([""]), do: nil
defp parse_rule([value]) do
%{directive: "",
value: String.trim(value),
important?: String.contains?(value, "!important")}
end
@doc """
Merges CSS rules.
## Examples
iex> "p {background-color: #fff !important; color: #000;} p {background-color: #000;}" |> Premailex.CSSParser.parse() |> Premailex.CSSParser.merge()
[%{directive: "background-color", value: "#fff !important", important?: true, specificity: 1},
%{directive: "color", value: "#000", important?: false, specificity: 1}]
"""
def merge(rule_sets) do
rule_sets
|> Enum.map(fn rule_set ->
Enum.map(rule_set.rules, &Map.put(&1, :specificity, rule_set.specificity))
end)
|> Enum.reduce([], &Enum.concat(&2, &1))
|> merge_rule_sets
end
defp merge_rule_sets(rule_sets) do
rule_sets
|> Enum.reduce(%{}, &merge_into_rule_set(&2, &1))
|> Enum.into([], &elem(&1, 1))
end
defp merge_into_rule_set(rule_set, new_rule) do
rule = rule_set |> Map.get(new_rule.directive, nil)
# Cascading order: http://www.w3.org/TR/CSS21/cascade.html#cascading-order
cond do
is_nil(rule) ->
Map.put(rule_set, new_rule.directive, new_rule)
new_rule.important? and (!rule.important? or rule.specificity <= new_rule.specificity) ->
Map.put(rule_set, new_rule.directive, new_rule)
!rule.important? and rule.specificity <= new_rule.specificity ->
Map.put(rule_set, new_rule.directive, new_rule)
true ->
rule_set
end
end
@doc """
Transforms CSS map or list into string.
## Examples
iex> Premailex.CSSParser.to_string([%{directive: "background-color", value: "#fff"}, %{directive: "color", value: "#000"}])
"background-color:#fff;color:#000;"
iex> Premailex.CSSParser.to_string(%{directive: "background-color", value: "#fff"})
"background-color:#fff;"
"""
def to_string(rules) when is_list(rules),
do: Enum.reduce(rules, "", &"#{&2}#{__MODULE__.to_string(&1)}")
def to_string(%{directive: directive, value: value}),
do: "#{directive}:#{value};"
defp calculate_specificity(selector) do
b = ~r/\#/ |> Regex.scan(selector) |> length()
c = @non_id_attributes_and_pseudo_classes |> Regex.scan(selector) |> length()
d = @elements_and_pseudo_elements |> Regex.scan(selector) |> length()
b + c + d
end
end