defmodule Floki.Selector.Parser do require Logger @moduledoc false # Parses a list of tokens returned from `Tokenizer` and transform into a `Selector`. alias Floki.Selector alias Selector.{Functional, Tokenizer, PseudoClass, AttributeSelector, Combinator} @attr_match_types [ :equal, :dash_match, :includes, :prefix_match, :suffix_match, :substring_match ] # Returns a list of `Selector` structs with the parsed selectors. def parse(selector) when is_binary(selector) do token_list = Tokenizer.tokenize(selector) parse(token_list) end def parse(tokens) do do_parse_all(tokens, []) end defp do_parse_all([], selectors) do Enum.reverse(selectors) end defp do_parse_all(tokens, selectors) do {selector, remaining_tokens} = do_parse(tokens, %Selector{}) do_parse_all(remaining_tokens, [selector | selectors]) end defp do_parse([], selector), do: {optimize_selector(selector), []} defp do_parse([{:close_parentesis, _} | t], selector), do: {optimize_selector(selector), t} defp do_parse([{:comma, _} | t], selector), do: {optimize_selector(selector), t} defp do_parse([{:identifier, _, namespace}, {:namespace_pipe, _} | t], selector) do do_parse(t, %{selector | namespace: to_string(namespace)}) end defp do_parse([{:identifier, _, type} | t], selector) do do_parse(t, %{selector | type: to_string(type)}) end defp do_parse([{~c"*", _} | t], selector) do do_parse(t, %{selector | type: "*"}) end defp do_parse([{:hash, _, id} | t], selector) do do_parse(t, %{selector | id: to_string(id)}) end defp do_parse([{:class, _, class} | t], selector) do do_parse(t, %{selector | classes: [to_string(class) | selector.classes]}) end defp do_parse([{~c"[", _} | t], selector) do {t, result} = consume_attribute(t) do_parse(t, %{selector | attributes: [result | selector.attributes]}) end defp do_parse([{:pseudo_not, _} | t], selector) do {t, pseudo_not_class} = do_parse_pseudo_not(t, %PseudoClass{name: "not", value: []}) pseudo_classes = Enum.reject([pseudo_not_class | selector.pseudo_classes], &is_nil(&1)) do_parse(t, %{selector | pseudo_classes: pseudo_classes}) end defp do_parse([{:pseudo, _, pseudo_class} | t], selector) do pseudo_classes = [%PseudoClass{name: to_string(pseudo_class)} | selector.pseudo_classes] do_parse(t, %{selector | pseudo_classes: pseudo_classes}) end defp do_parse([{:pseudo_class_int, _, pseudo_class_int} | t], selector) do [pseudo_class | pseudo_classes] = selector.pseudo_classes do_parse(t, %{ selector | pseudo_classes: [%{pseudo_class | value: pseudo_class_int} | pseudo_classes] }) end defp do_parse([{:pseudo_class_even, _} | t], selector) do [pseudo_class | pseudo_classes] = selector.pseudo_classes do_parse(t, %{selector | pseudo_classes: [%{pseudo_class | value: "even"} | pseudo_classes]}) end defp do_parse([{:pseudo_class_odd, _} | t], selector) do [pseudo_class | pseudo_classes] = selector.pseudo_classes do_parse(t, %{selector | pseudo_classes: [%{pseudo_class | value: "odd"} | pseudo_classes]}) end defp do_parse([{:pseudo_class_pattern, _, pattern} | t], selector) do [pseudo_class | pseudo_classes] = selector.pseudo_classes value = case Functional.parse(pattern) do :invalid -> to_string(pattern) {:ok, value} -> value end do_parse(t, %{selector | pseudo_classes: [%{pseudo_class | value: value} | pseudo_classes]}) end defp do_parse([{:pseudo_class_quoted, _, pattern} | t], selector) do [pseudo_class | pseudo_classes] = selector.pseudo_classes do_parse(t, %{ selector | pseudo_classes: [%{pseudo_class | value: to_string(pattern)} | pseudo_classes] }) end defp do_parse([{:space, _} | t], selector) do {remaining_tokens, combinator} = consume_combinator(t, :descendant) {%{selector | combinator: combinator}, remaining_tokens} end defp do_parse([{:greater, _} | t], selector) do {remaining_tokens, combinator} = consume_combinator(t, :child) {%{selector | combinator: combinator}, remaining_tokens} end defp do_parse([{:plus, _} | t], selector) do {remaining_tokens, combinator} = consume_combinator(t, :adjacent_sibling) {%{selector | combinator: combinator}, remaining_tokens} end defp do_parse([{:tilde, _} | t], selector) do {remaining_tokens, combinator} = consume_combinator(t, :general_sibling) {%{selector | combinator: combinator}, remaining_tokens} end defp do_parse([{:unknown, _, unknown} | t], selector) do Logger.debug(fn -> "Unknown token #{inspect(unknown)}. Ignoring." end) do_parse(t, selector) end defp consume_attribute(tokens), do: consume_attribute(:consuming, tokens, %AttributeSelector{}) defp consume_attribute(_, [], attr_selector), do: {[], attr_selector} defp consume_attribute(:done, tokens, attr_selector), do: {tokens, attr_selector} defp consume_attribute(:consuming, [{:identifier, _, identifier} | t], attr_selector) do new_selector = set_attribute_name_or_value(attr_selector, identifier) consume_attribute(:consuming, t, new_selector) end defp consume_attribute(:consuming, [{match_type, _} | t], attr_selector) when match_type in @attr_match_types do new_selector = %{attr_selector | match_type: match_type} consume_attribute(:consuming, t, new_selector) end defp consume_attribute(:consuming, [{:quoted, _, value} | t], attr_selector) do new_selector = %{attr_selector | value: to_string(value)} consume_attribute(:consuming, t, new_selector) end defp consume_attribute(:consuming, [{:attribute_identifier, _, value} | t], attr_selector) do flag = String.at(to_string(value), -2) new_selector = %{attr_selector | flag: flag} consume_attribute(:done, t, new_selector) end defp consume_attribute(:consuming, [{~c"]", _} | t], attr_selector) do consume_attribute(:done, t, attr_selector) end defp consume_attribute(:consuming, [unknown | t], attr_selector) do Logger.debug(fn -> "Unknown token #{inspect(unknown)}. Ignoring." end) consume_attribute(:consuming, t, attr_selector) end defp set_attribute_name_or_value(attr_selector, identifier) do # When match type is not defined, this is an attribute name. # Otherwise, it is an attribute value. case attr_selector.match_type do nil -> %{attr_selector | attribute: to_string(identifier)} _ -> %{attr_selector | value: to_string(identifier)} end end defp consume_combinator(tokens, combinator_type) when is_atom(combinator_type) do consume_combinator(tokens, %Combinator{match_type: combinator_type, selector: %Selector{}}) end defp consume_combinator([], combinator), do: {[], combinator} defp consume_combinator(tokens, combinator) do {selector, remaining_tokens} = do_parse(tokens, %Selector{}) {remaining_tokens, %{combinator | selector: selector}} end defp do_parse_pseudo_not([], pseudo_class) do {[], pseudo_class} end defp do_parse_pseudo_not([{:close_parentesis, _} | t], pseudo_class) do {t, pseudo_class} end defp do_parse_pseudo_not([{:space, _} | t], pseudo_class) do do_parse_pseudo_not(t, pseudo_class) end # At this point we want to ignore comma, because it's going to start a new selector. defp do_parse_pseudo_not([{:comma, _} | t], pseudo_class) do do_parse_pseudo_not(t, pseudo_class) end defp do_parse_pseudo_not(tokens, pseudo_class) do do_parse_pseudo_not(tokens, %Selector{}, pseudo_class) end defp do_parse_pseudo_not([], pseudo_not_selector, pseudo_class) do pseudo_class = update_pseudo_not_value(pseudo_class, pseudo_not_selector) {[], pseudo_class} end defp do_parse_pseudo_not([{:close_parentesis, _} | t], pseudo_not_selector, pseudo_class) do pseudo_class = update_pseudo_not_value(pseudo_class, pseudo_not_selector) {t, pseudo_class} end defp do_parse_pseudo_not([{:comma, _} | t], pseudo_not_selector, pseudo_class) do pseudo_class = update_pseudo_not_value(pseudo_class, pseudo_not_selector) do_parse_pseudo_not(t, pseudo_class) end defp do_parse_pseudo_not([{:space, _} | t], pseudo_not_selector, pseudo_class) do do_parse_pseudo_not(t, pseudo_not_selector, pseudo_class) end defp do_parse_pseudo_not([{~c"[", _} | tokens], pseudo_not_selector, pseudo_class) do {remaining_tokens, result} = consume_attribute(tokens) selector = %{pseudo_not_selector | attributes: [result | pseudo_not_selector.attributes]} pseudo_class = update_pseudo_not_value(pseudo_class, selector) do_parse_pseudo_not(remaining_tokens, pseudo_class) end defp do_parse_pseudo_not([next_token | t], pseudo_not_selector, pseudo_class) do {pseudo_not_selector, _} = do_parse([next_token], pseudo_not_selector) do_parse_pseudo_not(t, pseudo_not_selector, pseudo_class) end defp update_pseudo_not_value(pseudo_class, pseudo_not_selector = %Selector{combinator: nil}) do pseudo_not_value = [pseudo_not_selector | Map.get(pseudo_class, :value, [])] %{pseudo_class | value: pseudo_not_value} end defp update_pseudo_not_value(_pseudo_class, _pseudo_not_selector) do Logger.debug("Only simple selectors are allowed in :not() pseudo-class. Ignoring.") nil end # Reorders classes in selector to improve matching performance. defp optimize_selector(selector) do %{selector | classes: Enum.sort(selector.classes, &(bit_size(&1) >= bit_size(&2)))} end end