# 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 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, rest: nil, relationship: nil)
@selector = selector
@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)
matching_nodes = find_nodes_without_relationship(node, descendant_match: descendant_match)
if @relationship.nil?
return matching_nodes
end
expression_nodes = matching_nodes.map do |matching_node|
case @relationship
when :descendant
nodes = []
matching_node.recursive_children { |child_node|
nodes += @rest.query_nodes(child_node, descendant_match: false)
}
nodes
when :child
matching_node.children.map { |child_node| @rest.query_nodes(child_node, descendant_match: false) }
.flatten
when :next_sibling
@rest.query_nodes(matching_node.siblings.first, descendant_match: false)
when :subsequent_sibling
matching_node.siblings.map { |sibling_node| @rest.query_nodes(sibling_node, descendant_match: false) }
.flatten
end
end.flatten
end
def to_s
return @selector.to_s unless @rest
result = []
result << @selector if @selector
case @relationship
when :child then result << '>'
when :subsequent_sibling then result << '~'
when :next_sibling then result << '+'
end
result << @rest
result.map(&:to_s).join(' ')
end
private
def find_nodes_without_relationship(node, descendant_match: true)
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