diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb index fd8e2202fb..e32e299b4d 100644 --- a/app/lib/search_query_parser.rb +++ b/app/lib/search_query_parser.rb @@ -1,11 +1,47 @@ # frozen_string_literal: true class SearchQueryParser < Parslet::Parser + # Efficiently matches disjoint strings + class StrList < Parslet::Atoms::Base + attr_reader :strings + + def initialize(strings) + super() + + @strings = strings + @pattern = Regexp.union(strings) + @min_length = strings.map(&:length).min + end + + def error_msgs + @error_msgs ||= { + premature: 'Premature end of input', + failed: "Expected any of #{strings.inspect}, but got ", + } + end + + def try(source, context, _consume_all) + match = source.match(@pattern) + return succ(source.consume(match)) unless match.nil? + + # Input ending early: + return context.err(self, source, error_msgs[:premature]) if source.chars_left < @min_length + + # Expected something, but got something else instead: + error_pos = source.pos + context.err_at(self, source, [error_msgs[:failed], source.consume(@len)], error_pos) + end + + def to_s_inner(_prec) + "[#{strings.map { |str| "'#{str}'" }.join(',')}]" + end + end + rule(:term) { match('[^\s]').repeat(1).as(:term) } rule(:colon) { str(':') } rule(:space) { match('\s').repeat(1) } rule(:operator) { (str('+') | str('-')).as(:operator) } - rule(:prefix_operator) { str('has') | str('is') | str('language') | str('from') | str('before') | str('after') | str('during') | str('in') } + rule(:prefix_operator) { StrList.new(%w(has is language from before after during in)) } rule(:prefix) { prefix_operator.as(:prefix_operator) >> colon } rule(:phrase) do (str('"') >> match('[^"]').repeat.as(:phrase) >> str('"')) |