1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
# Syntax notes for search:
# you can put a - in front of any search term to negate it
# you can scope a search by putting a name of a scope, a colon and then the search term WITHOUT a space.
# scoping will allow you to search for more things than otherwise available
# an unknown scope name will be assumed to be a header to search
# you can surround a search term in quotes to search for the whole thing
# multiple search terms will be ANDed together
# you can OR things by using the keyword OR/or - if you have it without parens, you will or the whole left with the whole right, until we find another or.
# if you use parenthesis, you can group together terms
# search in:_default_, in:all, in:trash, in:sent, in:drafts will only work for the WHOLE search. You can do a negation on a scoped search if it's in:trash, in:sent or in:drafts, but not for in:all
module PixelatedService
class Search
def initialize(q)
if q
@qtree, @search_scope = Search.compile(q)
else
@qtree, @search_scope = TrueMatch.new, PixelatedService::MailScopeFilter::Default
end
end
def restrict(input)
@search_scope.new(input).select do |mm|
@qtree.match?(mm)
end
end
REGEXP_DQUOTED = /"[^"]*"/
REGEXP_SQUOTED = /'[^']*'/
REGEXP_SCOPE = /\w+:(".*?"|'.*?'|[^\s\)]+)/
REGEXP_OTHER = /[^\s\)]+/
def self.scan_literal(qs)
if qs.check(REGEXP_DQUOTED)
StringMatch.new(qs.scan(REGEXP_DQUOTED), true)
elsif qs.check(REGEXP_SQUOTED)
StringMatch.new(qs.scan(REGEXP_SQUOTED), true)
elsif qs.check(REGEXP_OTHER)
StringMatch.new(qs.scan(REGEXP_OTHER))
end
end
def self.combine_search_scopes(l, r)
l + r
end
def self.compile(q, qs = StringScanner.new(q))
qtree = AndMatch.new
search_scope = PixelatedService::MailScopeFilter::Default
until qs.eos?
if qs.check(/\)/)
qs.scan(/\)/)
return optimized(qtree), search_scope
end
negated = false
if qs.check(/-/)
negated = true
qs.scan(/-/)
end
if qs.check(/or/i)
qs.scan(/or/i)
left = qtree
qtree = OrMatch.new(left, AndMatch.new)
else
res =
if qs.check(/\(/)
qs.scan(/\(/)
v, sc = compile(q, qs)
search_scope = search_scope + sc
v
elsif qs.check(REGEXP_DQUOTED)
StringMatch.new(qs.scan(REGEXP_DQUOTED), true)
elsif qs.check(REGEXP_SQUOTED)
StringMatch.new(qs.scan(REGEXP_SQUOTED), true)
elsif qs.check(REGEXP_SCOPE)
scope = qs.scan(/\w+/)
qs.scan(/:/)
rest_node = scan_literal(qs)
v = ScopeMatch.new(scope, rest_node)
if v.is_search_scope? && !negated
search_scope = search_scope + v.search_scope
TrueMatch.new
else
v
end
elsif qs.check(REGEXP_OTHER)
StringMatch.new(qs.scan(REGEXP_OTHER))
end
res = NegateMatch.new(res) if negated
qtree << res
end
qs.scan(/\s+/)
end
return optimized(qtree), search_scope
end
def self.optimized(tree)
case tree
when AndMatch
data = tree.data.reject { |d| TrueMatch === d }
if data.length == 1
optimized(data.first)
else
AndMatch.new(data.map { |n| optimized(n)} )
end
when OrMatch
if tree.right.is_a?(AndMatch) && tree.right.data.empty?
optimized(tree.left)
else
OrMatch.new(optimized(tree.left), optimized(tree.right))
end
when NegateMatch
if tree.data.is_a?(NegateMatch)
optimized(tree.data.data)
else
NegateMatch.new(optimized(tree.data))
end
else
tree
end
end
end
end
require 'pixelated_service/search/string_match'
require 'pixelated_service/search/scope_match'
require 'pixelated_service/search/negate_match'
require 'pixelated_service/search/and_match'
require 'pixelated_service/search/or_match'
require 'pixelated_service/search/true_match'
|