eric-keyword_search 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +36 -0
- data/Manifest +8 -0
- data/README.rdoc +100 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/keyword_search.gemspec +45 -0
- data/lib/keyword_search.rb +1315 -0
- data/lib/keyword_search.rl +112 -0
- data/lib/keyword_search/definition.rb +59 -0
- data/test/test_keyword_search.rb +319 -0
- metadata +66 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.dirname(__FILE__) << '/keyword_search/definition.rb'
|
2
|
+
|
3
|
+
module KeywordSearch
|
4
|
+
|
5
|
+
class ParseError < ::SyntaxError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
%%{
|
10
|
+
|
11
|
+
machine parser;
|
12
|
+
|
13
|
+
action start {
|
14
|
+
tokstart = p;
|
15
|
+
}
|
16
|
+
|
17
|
+
action key {
|
18
|
+
key = word
|
19
|
+
results[key] ||= []
|
20
|
+
}
|
21
|
+
|
22
|
+
action default {
|
23
|
+
key = nil
|
24
|
+
}
|
25
|
+
|
26
|
+
action word {
|
27
|
+
word = data[tokstart..p-1]
|
28
|
+
}
|
29
|
+
|
30
|
+
action value {
|
31
|
+
(results[key || :default] ||= []) << [ word, positive_match ]
|
32
|
+
}
|
33
|
+
|
34
|
+
action negative_match {
|
35
|
+
positive_match = false
|
36
|
+
}
|
37
|
+
|
38
|
+
action positive_match {
|
39
|
+
positive_match = true
|
40
|
+
}
|
41
|
+
|
42
|
+
action quote { quotes += 1 }
|
43
|
+
|
44
|
+
action unquote { quotes -= 1 }
|
45
|
+
|
46
|
+
seperators = ' '+ | / *[,|] */ ;
|
47
|
+
|
48
|
+
bareword = ( [^ '"(:] . [^ "):]* ) > start % word ; # allow apostrophes
|
49
|
+
dquoted = '"' @ quote ( [^"]* > start % word ) :>> '"' @ unquote;
|
50
|
+
squoted = '\'' @ quote ( [^']* > start % word ) :>> '\'' @ unquote;
|
51
|
+
|
52
|
+
anyword = dquoted | squoted | bareword ;
|
53
|
+
|
54
|
+
anyvalue = anyword % value ;
|
55
|
+
multivalues = anyvalue ( seperators anyvalue )* ;
|
56
|
+
groupedvalues = '(' @ quote multivalues :>> ')' @ unquote;
|
57
|
+
|
58
|
+
value = groupedvalues | anyvalue ;
|
59
|
+
|
60
|
+
pair = bareword % key ':' value ;
|
61
|
+
|
62
|
+
value_only = value > default ;
|
63
|
+
|
64
|
+
match_mode = ('-' % negative_match | '+'? % positive_match ) ;
|
65
|
+
|
66
|
+
definition = match_mode? <: ( pair | value_only ) ;
|
67
|
+
|
68
|
+
definitions = definition ( ' '+ definition )*;
|
69
|
+
|
70
|
+
main := ' '* definitions? ' '* 0
|
71
|
+
@!{ raise ParseError, "At offset #{p}, near: '#{data[p,10]}'" };
|
72
|
+
|
73
|
+
}%%
|
74
|
+
|
75
|
+
def search(input_string, definition=nil, &block)
|
76
|
+
definition ||= Definition.new(&block)
|
77
|
+
results = parse(input_string)
|
78
|
+
results.each do |key, terms|
|
79
|
+
definition.handle(key, terms)
|
80
|
+
end
|
81
|
+
results
|
82
|
+
end
|
83
|
+
|
84
|
+
#######
|
85
|
+
private
|
86
|
+
#######
|
87
|
+
|
88
|
+
def parse(input) #:nodoc:
|
89
|
+
data = input + ' '
|
90
|
+
%% write data;
|
91
|
+
p = 0
|
92
|
+
eof = nil
|
93
|
+
word = nil
|
94
|
+
pe = data.length
|
95
|
+
key = nil
|
96
|
+
positive_match = nil
|
97
|
+
tokstart = nil
|
98
|
+
results = {}
|
99
|
+
quotes = 0
|
100
|
+
%% write init;
|
101
|
+
%% write exec;
|
102
|
+
unless quotes.zero?
|
103
|
+
raise ParseError, "Unclosed quotes"
|
104
|
+
end
|
105
|
+
results
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module KeywordSearch
|
2
|
+
|
3
|
+
class Definition
|
4
|
+
|
5
|
+
class Keyword
|
6
|
+
|
7
|
+
attr_reader :name, :description, :handler
|
8
|
+
def initialize(name, description=nil, &handler)
|
9
|
+
@name, @description = name, description
|
10
|
+
@handler = handler
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle(value, sign)
|
14
|
+
# If the handler is only expecting one argument,
|
15
|
+
# only give them the positive matches
|
16
|
+
if handler.arity == 1
|
17
|
+
handler.call(value) if sign
|
18
|
+
else
|
19
|
+
handler.call(value, sign)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@default_keyword = nil
|
27
|
+
yield self if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def keywords
|
31
|
+
@keywords ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def keyword(name, description=nil, &block)
|
35
|
+
keywords << Keyword.new(name, description, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_keyword(name)
|
39
|
+
@default_keyword = name
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle(key, values)
|
43
|
+
key = @default_keyword if key == :default
|
44
|
+
return false unless key
|
45
|
+
true_values, false_values = *values.partition { |v| v[1] }
|
46
|
+
|
47
|
+
# Get just the values
|
48
|
+
true_values.collect! { |v| v[0] }
|
49
|
+
false_values.collect! { |v| v[0] }
|
50
|
+
|
51
|
+
if k = keywords.detect { |kw| kw.name == key.to_sym}
|
52
|
+
k.handle(true_values, true)
|
53
|
+
k.handle(false_values, false) if false_values.length > 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require 'rubygems' rescue nil
|
4
|
+
require 'test/spec'
|
5
|
+
|
6
|
+
require File.dirname(__FILE__) + '/../lib/keyword_search'
|
7
|
+
|
8
|
+
context "KeywordSearch" do
|
9
|
+
|
10
|
+
NAME_AND_AGE = %<bruce williams age:26>
|
11
|
+
NAME_QUOTED_AND_AGE = %<"bruce williams" age:26>
|
12
|
+
NAME_AND_QUOTED_AGE = %<bruce williams age:"26">
|
13
|
+
DEFAULT_AGE_WITH_QUOTED_AGE = %<26 name:"bruce williams">
|
14
|
+
DEFAULT_AGE_WITH_SINGLE_QUOTED_AGE = %<26 name:'bruce williams'>
|
15
|
+
NAME_WITH_NESTED_SINGLE_QUOTES = %<"d'arcy d'uberville" age:28>
|
16
|
+
NAME_AND_GROUPED_AGE = %<coda hale age:(27)>
|
17
|
+
NAME_AND_GROUPED_QUOTED_AGE = %<coda hale age:("27")>
|
18
|
+
NAME_AND_GROUPED_QUOTED_AGES = %<coda hale age:("27" 34 '48')>
|
19
|
+
GROUPED_NAMES_AND_AGE = %<(coda bruce 'hale' "williams") age:20>
|
20
|
+
|
21
|
+
specify "default keyword" do
|
22
|
+
result = nil
|
23
|
+
KeywordSearch.search(NAME_AND_AGE) do |with|
|
24
|
+
with.default_keyword :name
|
25
|
+
with.keyword :name do |values|
|
26
|
+
result = values.join(' ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
assert_equal 'bruce williams', result
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "grouped default keywords" do
|
33
|
+
result = nil
|
34
|
+
KeywordSearch.search(GROUPED_NAMES_AND_AGE) do |with|
|
35
|
+
with.default_keyword :name
|
36
|
+
with.keyword :name do |values|
|
37
|
+
result = values
|
38
|
+
end
|
39
|
+
end
|
40
|
+
assert_equal ['coda', 'bruce', 'hale', 'williams'], result
|
41
|
+
end
|
42
|
+
|
43
|
+
specify "unquoted keyword term" do
|
44
|
+
result = nil
|
45
|
+
KeywordSearch.search(NAME_AND_AGE) do |with|
|
46
|
+
with.keyword :age do |values|
|
47
|
+
result = Integer(values.first)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
assert_equal 26, result
|
51
|
+
end
|
52
|
+
|
53
|
+
specify "unquoted grouped keyword term" do
|
54
|
+
result = nil
|
55
|
+
KeywordSearch.search(NAME_AND_GROUPED_AGE) do |with|
|
56
|
+
with.keyword :age do |values|
|
57
|
+
result = Integer(values.first)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
assert_equal 27, result
|
61
|
+
end
|
62
|
+
|
63
|
+
specify "quoted grouped keyword term" do
|
64
|
+
result = nil
|
65
|
+
KeywordSearch.search(NAME_AND_GROUPED_QUOTED_AGE) do |with|
|
66
|
+
with.keyword :age do |values|
|
67
|
+
result = Integer(values.first)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
assert_equal 27, result
|
71
|
+
end
|
72
|
+
|
73
|
+
specify "mixed grouped keyword terms" do
|
74
|
+
result = nil
|
75
|
+
KeywordSearch.search(NAME_AND_GROUPED_QUOTED_AGES) do |with|
|
76
|
+
with.keyword :age do |values|
|
77
|
+
result = values.map { |v| v.to_i }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
assert_equal [27, 34, 48], result
|
81
|
+
end
|
82
|
+
|
83
|
+
specify "quoted default keyword term" do
|
84
|
+
result = nil
|
85
|
+
KeywordSearch.search(NAME_QUOTED_AND_AGE) do |with|
|
86
|
+
with.default_keyword :name
|
87
|
+
with.keyword :name do |values|
|
88
|
+
result = values.join(' ')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
assert_equal 'bruce williams', result
|
92
|
+
end
|
93
|
+
|
94
|
+
specify "quoted keyword term" do
|
95
|
+
result = nil
|
96
|
+
KeywordSearch.search(NAME_AND_QUOTED_AGE) do |with|
|
97
|
+
with.keyword :age do |values|
|
98
|
+
result = Integer(values.first)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
assert_equal 26, result
|
102
|
+
end
|
103
|
+
|
104
|
+
specify "quoted keyword term with whitespace" do
|
105
|
+
result = nil
|
106
|
+
KeywordSearch.search(DEFAULT_AGE_WITH_QUOTED_AGE) do |with|
|
107
|
+
with.default_keyword :age
|
108
|
+
with.keyword :name do |values|
|
109
|
+
result = values.first
|
110
|
+
end
|
111
|
+
end
|
112
|
+
assert_equal 'bruce williams', result
|
113
|
+
end
|
114
|
+
|
115
|
+
specify "single quoted keyword term with whitespace" do
|
116
|
+
result = nil
|
117
|
+
r = KeywordSearch.search(DEFAULT_AGE_WITH_SINGLE_QUOTED_AGE) do |with|
|
118
|
+
with.default_keyword :age
|
119
|
+
with.keyword :name do |values|
|
120
|
+
result = values.first
|
121
|
+
end
|
122
|
+
end
|
123
|
+
assert_equal 'bruce williams', result
|
124
|
+
end
|
125
|
+
|
126
|
+
specify "nested single quote is accumulated" do
|
127
|
+
result = nil
|
128
|
+
KeywordSearch.search(NAME_WITH_NESTED_SINGLE_QUOTES) do |with|
|
129
|
+
with.default_keyword :name
|
130
|
+
with.keyword :name do |values|
|
131
|
+
result = values.first
|
132
|
+
end
|
133
|
+
end
|
134
|
+
assert_equal %<d'arcy d'uberville>, result
|
135
|
+
end
|
136
|
+
|
137
|
+
specify "nested double quote is accumulated" do
|
138
|
+
result = nil
|
139
|
+
KeywordSearch.search(%<'he was called "jake"'>) do |with|
|
140
|
+
with.default_keyword :text
|
141
|
+
with.keyword :text do |values|
|
142
|
+
result = values.first
|
143
|
+
end
|
144
|
+
end
|
145
|
+
assert_equal %<he was called "jake">, result
|
146
|
+
end
|
147
|
+
|
148
|
+
specify "bare single quote in unquoted literal is accumulated" do
|
149
|
+
result = nil
|
150
|
+
KeywordSearch.search(%<bruce's age:27>) do |with|
|
151
|
+
with.default_keyword :text
|
152
|
+
with.keyword :text do |values|
|
153
|
+
result = values.first
|
154
|
+
end
|
155
|
+
end
|
156
|
+
assert_equal %<bruce's>, result
|
157
|
+
end
|
158
|
+
|
159
|
+
specify "single quoted literal is accumulated" do
|
160
|
+
result = nil
|
161
|
+
KeywordSearch.search(%<foo 'bruce williams' age:27>) do |with|
|
162
|
+
with.default_keyword :text
|
163
|
+
with.keyword :text do |values|
|
164
|
+
result = values.last
|
165
|
+
end
|
166
|
+
end
|
167
|
+
assert_equal %<bruce williams>, result
|
168
|
+
end
|
169
|
+
|
170
|
+
specify "period in literal is accumulated" do
|
171
|
+
result = nil
|
172
|
+
KeywordSearch.search(%<okay... age:27>) do |with|
|
173
|
+
with.default_keyword :text
|
174
|
+
with.keyword :text do |values|
|
175
|
+
result = values.first
|
176
|
+
end
|
177
|
+
end
|
178
|
+
assert_equal %<okay...>, result
|
179
|
+
end
|
180
|
+
|
181
|
+
specify "parse error results in exception" do
|
182
|
+
assert_raises(KeywordSearch::ParseError) do
|
183
|
+
KeywordSearch.search(%<we_do_not_allow:! or ::>) do |with|
|
184
|
+
with.default_keyword :text
|
185
|
+
with.keyword :text do |values|
|
186
|
+
result = values.first
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
specify "can use apostrophes in unquoted literal" do
|
193
|
+
result = nil
|
194
|
+
KeywordSearch.search(%<d'correct>) do |with|
|
195
|
+
with.default_keyword :text
|
196
|
+
with.keyword :text do |values|
|
197
|
+
result = values.first
|
198
|
+
end
|
199
|
+
end
|
200
|
+
assert_equal "d'correct", result
|
201
|
+
end
|
202
|
+
|
203
|
+
specify "can use apostrophes in unquoted literal values" do
|
204
|
+
result = nil
|
205
|
+
KeywordSearch.search(%<text:d'correct>) do |with|
|
206
|
+
with.default_keyword :text
|
207
|
+
with.keyword :text do |values|
|
208
|
+
result = values.first
|
209
|
+
end
|
210
|
+
end
|
211
|
+
assert_equal "d'correct", result
|
212
|
+
end
|
213
|
+
|
214
|
+
specify "cannot use an apostrophe at the beginning on an unquoted literal" do
|
215
|
+
assert_raises(KeywordSearch::ParseError) do
|
216
|
+
KeywordSearch.search(%<'thisiswrong>) do |with|
|
217
|
+
with.default_keyword :text
|
218
|
+
with.keyword :text do |values|
|
219
|
+
result = values.first
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
specify "keywords are case sensitive" do
|
226
|
+
result = nil
|
227
|
+
KeywordSearch.search(%<Text:justtesting>) do |with|
|
228
|
+
with.keyword :text do |values|
|
229
|
+
result = :small
|
230
|
+
end
|
231
|
+
with.keyword :Text do |values|
|
232
|
+
result = :big
|
233
|
+
end
|
234
|
+
end
|
235
|
+
assert_equal :big, result
|
236
|
+
end
|
237
|
+
|
238
|
+
specify "values are case sensitive" do
|
239
|
+
result = nil
|
240
|
+
KeywordSearch.search(%<text:Big>) do |with|
|
241
|
+
with.keyword :text do |values|
|
242
|
+
result = values.first
|
243
|
+
end
|
244
|
+
end
|
245
|
+
assert_equal 'Big', result
|
246
|
+
end
|
247
|
+
|
248
|
+
specify "spaces are condensed" do
|
249
|
+
result = nil
|
250
|
+
KeywordSearch.search(%< this is some text >) do |with|
|
251
|
+
with.default_keyword :text
|
252
|
+
with.keyword :text do |values|
|
253
|
+
result = values
|
254
|
+
end
|
255
|
+
end
|
256
|
+
assert_equal [], result.select { |v| v.match(/ /) }
|
257
|
+
end
|
258
|
+
|
259
|
+
specify "an empty search is successful" do
|
260
|
+
result = nil
|
261
|
+
KeywordSearch.search(%<>) do |with|
|
262
|
+
with.default_keyword :text
|
263
|
+
with.keyword :text do |values|
|
264
|
+
result = values
|
265
|
+
end
|
266
|
+
end
|
267
|
+
assert_nil result
|
268
|
+
end
|
269
|
+
|
270
|
+
specify 'a negative search' do
|
271
|
+
result = nil
|
272
|
+
|
273
|
+
KeywordSearch.search(%<-site:google.com>) do |with|
|
274
|
+
with.keyword :site do |values, positive|
|
275
|
+
result = [ values, positive ]
|
276
|
+
end
|
277
|
+
end
|
278
|
+
assert_equal [ [ 'google.com' ], false ], result
|
279
|
+
end
|
280
|
+
|
281
|
+
specify 'a positive search' do
|
282
|
+
result = nil
|
283
|
+
|
284
|
+
KeywordSearch.search(%<+site:google.com>) do |with|
|
285
|
+
with.keyword :site do |values, positive|
|
286
|
+
result = [ values, positive ]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
assert_equal [ [ 'google.com' ], true ], result
|
290
|
+
end
|
291
|
+
|
292
|
+
specify 'a search with no sign' do
|
293
|
+
result = nil
|
294
|
+
|
295
|
+
KeywordSearch.search(%<site:google.com>) do |with|
|
296
|
+
with.keyword :site do |values, positive|
|
297
|
+
result = [ values, positive ]
|
298
|
+
end
|
299
|
+
end
|
300
|
+
assert_equal [ [ 'google.com' ], true ], result
|
301
|
+
end
|
302
|
+
|
303
|
+
specify 'a term should default to positive with no sign' do
|
304
|
+
result = nil
|
305
|
+
|
306
|
+
KeywordSearch.search(%<-site:google.com inurl:atom>) do |with|
|
307
|
+
with.keyword :inurl do |values, positive|
|
308
|
+
result = [ values, positive ]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
assert_equal [ [ 'atom' ], true ], result
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
|
317
|
+
|
318
|
+
|
319
|
+
|