Premailex.CSSParser (Premailex v1.0.0)

Copy Markdown View Source

CSS parser.

Parser limitations

  • At-rules (@media, @font-face, @import, etc.) and comments are stripped.

  • Specificity for :not(...), :is(...), and :where(...) is approximated as a single pseudo-class. In Selectors Level 4, specificity is derived from the argument (or is 0 for :where).

Summary

Types

A CSS rule declaration.

A CSS pseudo-class or pseudo-element.

A CSS rule with computed specificity.

A list of selector steps ordered from right to left.

A single element step in a selector.

CSS specificity, matching the Selectors Level 4 four-tuple model.

Functions

Combines CSS rules into a final list of declarations using the CSS Cascade Level 4 algorithm.

Parses a CSS string into a list of CSS rules.

Parses a CSS declaration block string into a list of declarations.

Parses a selector string into a list of selector groups.

Splits a comma-separated selector string into individual selectors.

Converts a rule declaration or list of declarations into a string.

Types

declaration()

@type declaration() :: %{
  property: String.t(),
  value: String.t(),
  important?: boolean()
}

A CSS rule declaration.

pseudo()

@type pseudo() :: %{
  :name => String.t(),
  :expression => String.t() | nil,
  :kind => :pseudo_class | :pseudo_element,
  optional(:nth) => {integer(), integer()} | :invalid
}

A CSS pseudo-class or pseudo-element.

Pseudo-classes in the an+b family (:nth-child, :nth-of-type, :nth-last-child, :nth-last-of-type) include an additional :nth field holding the parsed {a, b} coefficients, or :invalid if the expression is missing or cannot be parsed.

rule()

@type rule() :: %{
  declarations: [declaration()],
  selector: String.t(),
  specificity: specificity()
}

A CSS rule with computed specificity.

selector_group()

@type selector_group() :: [selector_group_step()]

A list of selector steps ordered from right to left.

selector_group_step()

@type selector_group_step() :: %{
  tag: String.t() | nil,
  id: String.t() | nil,
  classes: [String.t()],
  attrs: [String.t() | {String.t(), String.t()}],
  pseudos: [pseudo()],
  combinator: :descendant | :child | :adjacent | :sibling | :column | nil
}

A single element step in a selector.

specificity()

@type specificity() :: {0..1, non_neg_integer(), non_neg_integer(), non_neg_integer()}

CSS specificity, matching the Selectors Level 4 four-tuple model.

Structured as {inline?, ids, classes, elements}:

  • inline? - 1 if the style is inline.
  • ids - number of ID selectors.
  • classes - number of class, attribute, and pseudo-class selectors.
  • elements - number of tag and pseudo-element selectors.

Functions

cascade(rules)

@spec cascade([rule()]) :: [declaration()]

Combines CSS rules into a final list of declarations using the CSS Cascade Level 4 algorithm.

Conflicts between declarations for the same property are resolved by !important and then by specificity.

See https://www.w3.org/TR/css-cascade-4/#cascading for details.

Examples

iex> rules = Premailex.CSSParser.parse("p {background-color: #fff !important; color: #000;} p {background-color: #000;}")
iex> Premailex.CSSParser.cascade(rules)
[
  %{property: "background-color", value: "#fff !important", important?: true},
  %{property: "color", value: "#000", important?: false}
]

parse(css)

@spec parse(String.t()) :: [rule()]

Parses a CSS string into a list of CSS rules.

Ignores at-rules (e.g. @media, @font-face, etc.) and comments, as they are not relevant for inlining CSS.

Examples

iex> Premailex.CSSParser.parse("body { background-color: #fff !important; color: red; }")
[
  %{
    declarations: [
      %{property: "background-color", value: "#fff !important", important?: true},
      %{property: "color", value: "red", important?: false}
    ],
    selector: "body",
    specificity: {0, 0, 0, 1}
  }
]

parse_declaration_block(declaration_block)

@spec parse_declaration_block(String.t()) :: [declaration()]

Parses a CSS declaration block string into a list of declarations.

Each declaration is a map containing the property, value, and a !important flag.

Examples

iex> Premailex.CSSParser.parse_declaration_block("background-color: #fff; color: red;")
[
  %{property: "background-color", value: "#fff", important?: false},
  %{property: "color", value: "red", important?: false}
]

parse_selector_groups(selector)

@spec parse_selector_groups(String.t()) :: [selector_group()]

Parses a selector string into a list of selector groups.

Each selector group is a list of steps, where each step is a map containing the tag, id, classes, attributes, pseudo-classes/elements, and combinator. Steps are ordered right to left.

Selectors that fail to parse are dropped and a debug log is emitted.

Examples

iex> Premailex.CSSParser.parse_selector_groups("div.foo")
[[%{tag: "div", id: nil, classes: ["foo"], attrs: [], pseudos: [], combinator: nil}]]

iex> Premailex.CSSParser.parse_selector_groups("body > p")
[
  [
    %{tag: "p", id: nil, classes: [], attrs: [], pseudos: [], combinator: nil},
    %{tag: "body", id: nil, classes: [], attrs: [], pseudos: [], combinator: :child}
  ]
]

split_selector_groups(selector)

@spec split_selector_groups(String.t()) :: [String.t()]

Splits a comma-separated selector string into individual selectors.

Examples

iex> Premailex.CSSParser.split_selector_groups("div, .foo")
["div", ".foo"]

iex> Premailex.CSSParser.split_selector_groups("div > p, a[href]")
["div > p", "a[href]"]

to_string(declarations)

@spec to_string([declaration()] | declaration()) :: String.t()

Converts a rule declaration or list of declarations into a string.

Examples

iex> declarations = Premailex.CSSParser.parse_declaration_block("background-color: #fff !important; color: red;")
iex> Premailex.CSSParser.to_string(declarations)
"background-color: #fff !important; color: red;"

iex> declarations = Premailex.CSSParser.parse_declaration_block("background-color: #fff")
iex> Premailex.CSSParser.to_string(declarations)
"background-color: #fff;"