# frozen_string_literal: true
module Synvert::Core::NodeQuery::Compiler
# Expression represents a node query expression.
class Expression
# Initialize a Expression.
# @param selector [Synvert::Core::NodeQuery::Compiler::Selector] the selector
# @param goto_scope [String] goto scope
# @param rest [Synvert::Core::NodeQuery::Compiler::Expression] the rest expression
# @param relationship [Symbol] the relationship between the selector and rest expression, it can be :descendant
, :child
, :next_sibling
, :subsequent_sibling
or nil
.
def initialize(selector: nil, goto_scope: nil, rest: nil, relationship: nil)
@selector = selector
@goto_scope = goto_scope
@rest = rest
@relationship = relationship
end
# Check if the node matches the expression.
# @param node [Parser::AST::Node] the node
# @return [Boolean]
def match?(node)
!query_nodes(node).empty?
end
# Query nodes by the expression.
#
# * 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
# @param descendant_match [Boolean] whether to match in descendant node
# @return [Array] matching nodes.
def query_nodes(node, descendant_match = true)
return find_nodes_by_goto_scope(node) if @goto_scope
return find_nodes_by_relationship(node) if @relationship
matching_nodes = find_nodes_without_relationship(node, descendant_match)
return matching_nodes if @rest.nil?
matching_nodes.map { |matching_node| find_nodes_by_rest(matching_node, descendant_match) }
.flatten
end
def to_s
return @selector.to_s unless @rest
result = []
result << @selector.to_s if @selector
result << "<#{@goto_scope}>" if @goto_scope
case @relationship
when :child then result << "> #{@rest}"
when :subsequent_sibling then result << "~ #{@rest}"
when :next_sibling then result << "+ #{@rest}"
when :has then result << ":has(#{@rest})"
when :not_has then result << ":not_has(#{@rest})"
else result << @rest.to_s
end
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, false)
end
# Find ndoes by @relationship
# @param node [Parser::AST::Node] node to match
def find_nodes_by_relationship(node)
case @relationship
when :child
if node.is_a?(::Array)
return node.map { |each_node| find_nodes_by_rest(each_node) }
.flatten
else
return node.children.map { |each_node| find_nodes_by_rest(each_node) }
.flatten
end
when :next_sibling
return find_nodes_by_rest(node.siblings.first)
when :subsequent_sibling
return node.siblings.map { |each_node| find_nodes_by_rest(each_node) }
.flatten
when :has
return @rest.match?(node) ? [node] : []
when :not_has
return !@rest.match?(node) ? [node] : []
end
end
# Find nodes by @rest
# @param node [Parser::AST::Node] node to match
# @param descendant_match [Boolean] whether to match in descendant node
def find_nodes_by_rest(node, descendant_match = false)
@rest.query_nodes(node, descendant_match)
end
# Find nodes with nil relationship.
# @param node [Parser::AST::Node] node to match
# @param descendant_match [Boolean] whether to match in descendant node
def find_nodes_without_relationship(node, descendant_match = true)
if node.is_a?(::Array)
return node.map { |each_node|
find_nodes_without_relationship(each_node, descendant_match)
}.flatten
end
return [node] unless @selector
nodes = []
nodes << node if @selector.match?(node)
if descendant_match
node.recursive_children do |child_node|
nodes << child_node if @selector.match?(child_node)
end
end
@selector.filter(nodes)
end
end
end