# frozen_string_literal: true module Synvert::Core::NodeQuery::Compiler # Selector used to match nodes, it combines by node type and/or attribute list, plus index or has expression. class Selector # Initialize a Selector. # @param goto_scope [String] goto scope # @param relationship [Symbol] the relationship between the selectors, it can be descendant nil, child >, next sibling + or subsequent sibing ~. # @param rest [Synvert::Core::NodeQuery::Compiler::Selector] the rest selector # @param basic_selector [Synvert::Core::NodeQuery::Compiler::BasicSelector] the simple selector # @param attribute_list [Synvert::Core::NodeQuery::Compiler::AttributeList] the attribute list # @param pseudo_class [String] the pseudo class, can be has or not_has # @param pseudo_selector [Synvert::Core::NodeQuery::Compiler::Expression] the pseudo selector def initialize(goto_scope: nil, relationship: nil, rest: nil, basic_selector: nil, pseudo_class: nil, pseudo_selector: nil) @goto_scope = goto_scope @relationship = relationship @rest = rest @basic_selector = basic_selector @pseudo_class = pseudo_class @pseudo_selector = pseudo_selector end # Check if node matches the selector. # @param node [Parser::AST::Node] the node def match?(node) node.is_a?(::Parser::AST::Node) && (!@basic_selector || @basic_selector.match?(node)) && match_pseudo_class?(node) end # Query nodes by the selector. # # * If relationship is nil, it will match in all recursive child nodes and return matching nodes. # * If relationship is decendant, it will match in all recursive child nodes. # * If relationship is child, it will match in direct child nodes. # * If relationship is next sibling, it try to match next sibling node. # * If relationship is subsequent sibling, it will match in all sibling nodes. # @param node [Parser::AST::Node] node to match # @return [Array] matching nodes. def query_nodes(node) return find_nodes_by_relationship(node) if @relationship if node.is_a?(::Array) return node.flat_map { |child_node| query_nodes(child_node) } end return find_nodes_by_goto_scope(node) if @goto_scope nodes = [] nodes << node if match?(node) if @basic_selector node.recursive_children do |child_node| nodes << child_node if match?(child_node) end end nodes end def to_s result = [] result << "#{@goto_scope} " if @goto_scope result << "#{@relationship} " if @relationship result << @rest.to_s if @rest result << @basic_selector.to_s if @basic_selector result << ":#{@pseudo_class}(#{@pseudo_selector})" if @pseudo_class result.join('') end private # Find nodes by @goto_scope # @param node [Parser::AST::Node] node to match def find_nodes_by_goto_scope(node) @goto_scope.split('.').each { |scope| node = node.send(scope) } @rest.query_nodes(node) end # Find ndoes by @relationship # @param node [Parser::AST::Node] node to match def find_nodes_by_relationship(node) nodes = [] case @relationship when '>' if node.is_a?(::Array) node.each do |child_node| nodes << child_node if @rest.match?(child_node) end else node.children.each do |child_node| nodes << child_node if @rest.match?(child_node) end end when '+' next_sibling = node.siblings.first nodes << next_sibling if @rest.match?(next_sibling) when '~' node.siblings.each do |sibling_node| nodes << sibling_node if @rest.match?(sibling_node) end end nodes end def match_pseudo_class?(node) case @pseudo_class when 'has' !@pseudo_selector.query_nodes(node).empty? when 'not_has' @pseudo_selector.query_nodes(node).empty? else true end end end end