mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
207 lines
5.2 KiB
Ruby
207 lines
5.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'regexp_parser'
|
|
|
|
module Capybara
|
|
class Selector
|
|
# @api private
|
|
class RegexpDisassembler
|
|
def initialize(regexp)
|
|
@regexp = regexp
|
|
end
|
|
|
|
def alternated_substrings
|
|
@alternated_substrings ||= begin
|
|
or_strings = process(alternation: true)
|
|
remove_or_covered(or_strings)
|
|
or_strings.any?(&:empty?) ? [] : or_strings
|
|
end
|
|
end
|
|
|
|
def substrings
|
|
@substrings ||= begin
|
|
strs = process(alternation: false).first
|
|
remove_and_covered(strs)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def remove_and_covered(strings)
|
|
# delete_if is documented to modify the array after every block iteration - this doesn't appear to be true
|
|
# uniq the strings to prevent identical strings from removing each other
|
|
strings.uniq!
|
|
|
|
# If we have "ab" and "abcd" required - only need to check for "abcd"
|
|
strings.delete_if do |sub_string|
|
|
strings.any? do |cover_string|
|
|
next if sub_string.equal? cover_string
|
|
|
|
cover_string.include?(sub_string)
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_or_covered(or_series)
|
|
# If we are going to match `("a" and "b") or ("ade" and "bce")` it only makes sense to match ("a" and "b")
|
|
|
|
# Ensure minimum sets of strings are being or'd
|
|
or_series.each { |strs| remove_and_covered(strs) }
|
|
|
|
# Remove any of the alternated string series that fully contain any other string series
|
|
or_series.delete_if do |and_strs|
|
|
or_series.any? do |and_strs2|
|
|
next if and_strs.equal? and_strs2
|
|
|
|
remove_and_covered(and_strs + and_strs2) == and_strs
|
|
end
|
|
end
|
|
end
|
|
|
|
def process(alternation:)
|
|
strs = extract_strings(Regexp::Parser.parse(@regexp), alternation: alternation)
|
|
strs = collapse(combine(strs).map(&:flatten))
|
|
strs.each { |str| str.map!(&:upcase) } if @regexp.casefold?
|
|
strs
|
|
end
|
|
|
|
def combine(strs)
|
|
suffixes = [[]]
|
|
strs.reverse_each do |str|
|
|
if str.is_a? Set
|
|
prefixes = str.each_with_object([]) { |s, memo| memo.concat combine(s) }
|
|
|
|
result = []
|
|
prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
|
|
suffixes = result
|
|
else
|
|
suffixes.each { |arr| arr.unshift str }
|
|
end
|
|
end
|
|
suffixes
|
|
end
|
|
|
|
def collapse(strs)
|
|
strs.map do |substrings|
|
|
substrings.slice_before(&:nil?).map(&:join).reject(&:empty?).uniq
|
|
end
|
|
end
|
|
|
|
def extract_strings(expression, alternation: false)
|
|
Expression.new(expression).extract_strings(alternation)
|
|
end
|
|
|
|
# @api private
|
|
class Expression
|
|
def initialize(exp)
|
|
@exp = exp
|
|
end
|
|
|
|
def extract_strings(process_alternatives)
|
|
strings = []
|
|
each do |exp|
|
|
next strings.push(nil) if exp.optional? && !process_alternatives
|
|
|
|
next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives
|
|
|
|
strings.concat(exp.strings(process_alternatives))
|
|
end
|
|
strings
|
|
end
|
|
|
|
protected
|
|
|
|
def alternation?
|
|
(type == :meta) && !terminal?
|
|
end
|
|
|
|
def optional?
|
|
min_repeat.zero?
|
|
end
|
|
|
|
def terminal?
|
|
@exp.terminal?
|
|
end
|
|
|
|
def strings(process_alternatives)
|
|
if indeterminate?
|
|
[nil]
|
|
elsif terminal?
|
|
terminal_strings
|
|
elsif optional?
|
|
optional_strings
|
|
else
|
|
repeated_strings(process_alternatives)
|
|
end
|
|
end
|
|
|
|
def terminal_strings
|
|
text = case @exp.type
|
|
when :literal then @exp.text
|
|
when :escape then @exp.char
|
|
else
|
|
return [nil]
|
|
end
|
|
|
|
optional? ? options_set(text) : repeat_set(text)
|
|
end
|
|
|
|
def optional_strings
|
|
options_set(extract_strings(true))
|
|
end
|
|
|
|
def repeated_strings(process_alternatives)
|
|
repeat_set extract_strings(process_alternatives)
|
|
end
|
|
|
|
def alternative_strings
|
|
alts = alternatives.map { |sub_exp| sub_exp.extract_strings(alternation: true) }
|
|
alts.all?(&:any?) ? Set.new(alts) : nil
|
|
end
|
|
|
|
private
|
|
|
|
def indeterminate?
|
|
%i[meta set].include?(type)
|
|
end
|
|
|
|
def min_repeat
|
|
@exp.quantifier&.min || 1
|
|
end
|
|
|
|
def max_repeat
|
|
@exp.quantifier&.max || 1
|
|
end
|
|
|
|
def fixed_repeat?
|
|
min_repeat == max_repeat
|
|
end
|
|
|
|
def type
|
|
@exp.type
|
|
end
|
|
|
|
def repeat_set(str)
|
|
strs = Array(str * min_repeat)
|
|
strs.push(nil) unless fixed_repeat?
|
|
strs
|
|
end
|
|
|
|
def options_set(strs)
|
|
strs = [Set.new([[''], Array(strs)])]
|
|
strs.push(nil) unless max_repeat == 1
|
|
strs
|
|
end
|
|
|
|
def alternatives
|
|
@exp.alternatives.map { |exp| Expression.new(exp) }
|
|
end
|
|
|
|
def each
|
|
@exp.each { |exp| yield Expression.new(exp) }
|
|
end
|
|
end
|
|
private_constant :Expression
|
|
end
|
|
end
|
|
end
|