keyword_search_yjchen 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/Manifest +8 -0
- data/README.md +112 -0
- data/Rakefile +16 -0
- data/keyword_search.gemspec +24 -0
- data/lib/keyword_search/definition.rb +57 -0
- data/lib/keyword_search/version.rb +3 -0
- data/lib/keyword_search.rb +2289 -0
- data/lib/keyword_search.rl +112 -0
- data/test/test_keyword_search.rb +336 -0
- metadata +90 -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,336 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + '/../lib/keyword_search'
|
5
|
+
|
6
|
+
describe "KeywordSearch" do
|
7
|
+
|
8
|
+
NAME_AND_AGE = %<bruce williams age:26>
|
9
|
+
NAME_QUOTED_AND_AGE = %<"bruce williams" age:26>
|
10
|
+
NAME_AND_QUOTED_AGE = %<bruce williams age:"26">
|
11
|
+
DEFAULT_AGE_WITH_QUOTED_AGE = %<26 name:"bruce williams">
|
12
|
+
DEFAULT_AGE_WITH_SINGLE_QUOTED_AGE = %<26 name:'bruce williams'>
|
13
|
+
NAME_WITH_NESTED_SINGLE_QUOTES = %<"d'arcy d'uberville" age:28>
|
14
|
+
NAME_AND_GROUPED_AGE = %<coda hale age:(27)>
|
15
|
+
NAME_AND_GROUPED_QUOTED_AGE = %<coda hale age:("27")>
|
16
|
+
NAME_AND_GROUPED_QUOTED_AGES = %<coda hale age:("27" 34 '48')>
|
17
|
+
GROUPED_NAMES_AND_AGE = %<(coda bruce 'hale' "williams") age:20>
|
18
|
+
|
19
|
+
it "default keyword" do
|
20
|
+
result = nil
|
21
|
+
KeywordSearch.search(NAME_AND_AGE) do |with|
|
22
|
+
with.default_keyword :name
|
23
|
+
with.keyword :name do |values|
|
24
|
+
result = values.join(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
result.must_equal 'bruce williams'
|
28
|
+
assert_equal 'bruce williams', result
|
29
|
+
end
|
30
|
+
|
31
|
+
it "grouped default keywords" do
|
32
|
+
result = nil
|
33
|
+
KeywordSearch.search(GROUPED_NAMES_AND_AGE) do |with|
|
34
|
+
with.default_keyword :name
|
35
|
+
with.keyword :name do |values|
|
36
|
+
result = values
|
37
|
+
end
|
38
|
+
end
|
39
|
+
assert_equal ['coda', 'bruce', 'hale', 'williams'], result
|
40
|
+
end
|
41
|
+
|
42
|
+
it "unquoted keyword term" do
|
43
|
+
result = nil
|
44
|
+
KeywordSearch.search(NAME_AND_AGE) do |with|
|
45
|
+
with.keyword :age do |values|
|
46
|
+
result = Integer(values.first)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
assert_equal 26, result
|
50
|
+
end
|
51
|
+
|
52
|
+
it "unquoted grouped keyword term" do
|
53
|
+
result = nil
|
54
|
+
KeywordSearch.search(NAME_AND_GROUPED_AGE) do |with|
|
55
|
+
with.keyword :age do |values|
|
56
|
+
result = Integer(values.first)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
assert_equal 27, result
|
60
|
+
end
|
61
|
+
|
62
|
+
it "quoted grouped keyword term" do
|
63
|
+
result = nil
|
64
|
+
KeywordSearch.search(NAME_AND_GROUPED_QUOTED_AGE) do |with|
|
65
|
+
with.keyword :age do |values|
|
66
|
+
result = Integer(values.first)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
assert_equal 27, result
|
70
|
+
end
|
71
|
+
|
72
|
+
it "mixed grouped keyword terms" do
|
73
|
+
result = nil
|
74
|
+
KeywordSearch.search(NAME_AND_GROUPED_QUOTED_AGES) do |with|
|
75
|
+
with.keyword :age do |values|
|
76
|
+
result = values.map { |v| v.to_i }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
assert_equal [27, 34, 48], result
|
80
|
+
end
|
81
|
+
|
82
|
+
it "quoted default keyword term" do
|
83
|
+
result = nil
|
84
|
+
KeywordSearch.search(NAME_QUOTED_AND_AGE) do |with|
|
85
|
+
with.default_keyword :name
|
86
|
+
with.keyword :name do |values|
|
87
|
+
result = values.join(' ')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
assert_equal 'bruce williams', result
|
91
|
+
end
|
92
|
+
|
93
|
+
it "quoted keyword term" do
|
94
|
+
result = nil
|
95
|
+
KeywordSearch.search(NAME_AND_QUOTED_AGE) do |with|
|
96
|
+
with.keyword :age do |values|
|
97
|
+
result = Integer(values.first)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
assert_equal 26, result
|
101
|
+
end
|
102
|
+
|
103
|
+
it "quoted keyword term with whitespace" do
|
104
|
+
result = nil
|
105
|
+
KeywordSearch.search(DEFAULT_AGE_WITH_QUOTED_AGE) do |with|
|
106
|
+
with.default_keyword :age
|
107
|
+
with.keyword :name do |values|
|
108
|
+
result = values.first
|
109
|
+
end
|
110
|
+
end
|
111
|
+
assert_equal 'bruce williams', result
|
112
|
+
end
|
113
|
+
|
114
|
+
it "single quoted keyword term with whitespace" do
|
115
|
+
result = nil
|
116
|
+
r = KeywordSearch.search(DEFAULT_AGE_WITH_SINGLE_QUOTED_AGE) do |with|
|
117
|
+
with.default_keyword :age
|
118
|
+
with.keyword :name do |values|
|
119
|
+
result = values.first
|
120
|
+
end
|
121
|
+
end
|
122
|
+
assert_equal 'bruce williams', result
|
123
|
+
end
|
124
|
+
|
125
|
+
it "nested single quote is accumulated" do
|
126
|
+
result = nil
|
127
|
+
KeywordSearch.search(NAME_WITH_NESTED_SINGLE_QUOTES) do |with|
|
128
|
+
with.default_keyword :name
|
129
|
+
with.keyword :name do |values|
|
130
|
+
result = values.first
|
131
|
+
end
|
132
|
+
end
|
133
|
+
assert_equal %<d'arcy d'uberville>, result
|
134
|
+
end
|
135
|
+
|
136
|
+
it "nested double quote is accumulated" do
|
137
|
+
result = nil
|
138
|
+
KeywordSearch.search(%<'he was called "jake"'>) do |with|
|
139
|
+
with.default_keyword :text
|
140
|
+
with.keyword :text do |values|
|
141
|
+
result = values.first
|
142
|
+
end
|
143
|
+
end
|
144
|
+
assert_equal %<he was called "jake">, result
|
145
|
+
end
|
146
|
+
|
147
|
+
it "bare single quote in unquoted literal is accumulated" do
|
148
|
+
result = nil
|
149
|
+
KeywordSearch.search(%<bruce's age:27>) do |with|
|
150
|
+
with.default_keyword :text
|
151
|
+
with.keyword :text do |values|
|
152
|
+
result = values.first
|
153
|
+
end
|
154
|
+
end
|
155
|
+
assert_equal %<bruce's>, result
|
156
|
+
end
|
157
|
+
|
158
|
+
it "single quoted literal is accumulated" do
|
159
|
+
result = nil
|
160
|
+
KeywordSearch.search(%<foo 'bruce williams' age:27>) do |with|
|
161
|
+
with.default_keyword :text
|
162
|
+
with.keyword :text do |values|
|
163
|
+
result = values.last
|
164
|
+
end
|
165
|
+
end
|
166
|
+
assert_equal %<bruce williams>, result
|
167
|
+
end
|
168
|
+
|
169
|
+
it "period in literal is accumulated" do
|
170
|
+
result = nil
|
171
|
+
KeywordSearch.search(%<okay... age:27>) do |with|
|
172
|
+
with.default_keyword :text
|
173
|
+
with.keyword :text do |values|
|
174
|
+
result = values.first
|
175
|
+
end
|
176
|
+
end
|
177
|
+
assert_equal %<okay...>, result
|
178
|
+
end
|
179
|
+
|
180
|
+
it "parse error results in exception" do
|
181
|
+
assert_raises(KeywordSearch::ParseError) do
|
182
|
+
KeywordSearch.search(%<we_do_not_allow:! or ::>) do |with|
|
183
|
+
with.default_keyword :text
|
184
|
+
with.keyword :text do |values|
|
185
|
+
result = values.first
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it "can use apostrophes in unquoted literal" do
|
192
|
+
result = nil
|
193
|
+
KeywordSearch.search(%<d'correct>) do |with|
|
194
|
+
with.default_keyword :text
|
195
|
+
with.keyword :text do |values|
|
196
|
+
result = values.first
|
197
|
+
end
|
198
|
+
end
|
199
|
+
assert_equal "d'correct", result
|
200
|
+
end
|
201
|
+
|
202
|
+
it "can use apostrophes in unquoted literal values" do
|
203
|
+
result = nil
|
204
|
+
KeywordSearch.search(%<text:d'correct>) do |with|
|
205
|
+
with.default_keyword :text
|
206
|
+
with.keyword :text do |values|
|
207
|
+
result = values.first
|
208
|
+
end
|
209
|
+
end
|
210
|
+
assert_equal "d'correct", result
|
211
|
+
end
|
212
|
+
|
213
|
+
it "cannot use an apostrophe at the beginning on an unquoted literal" do
|
214
|
+
assert_raises(KeywordSearch::ParseError) do
|
215
|
+
KeywordSearch.search(%<'thisiswrong>) do |with|
|
216
|
+
with.default_keyword :text
|
217
|
+
with.keyword :text do |values|
|
218
|
+
result = values.first
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
it "keywords are case sensitive" do
|
225
|
+
result = nil
|
226
|
+
KeywordSearch.search(%<Text:justtesting>) do |with|
|
227
|
+
with.keyword :text do |values|
|
228
|
+
result = :small
|
229
|
+
end
|
230
|
+
with.keyword :Text do |values|
|
231
|
+
result = :big
|
232
|
+
end
|
233
|
+
end
|
234
|
+
assert_equal :big, result
|
235
|
+
end
|
236
|
+
|
237
|
+
it "values are case sensitive" do
|
238
|
+
result = nil
|
239
|
+
KeywordSearch.search(%<text:Big>) do |with|
|
240
|
+
with.keyword :text do |values|
|
241
|
+
result = values.first
|
242
|
+
end
|
243
|
+
end
|
244
|
+
assert_equal 'Big', result
|
245
|
+
end
|
246
|
+
|
247
|
+
it "spaces are condensed" do
|
248
|
+
result = nil
|
249
|
+
KeywordSearch.search(%< this is some text >) do |with|
|
250
|
+
with.default_keyword :text
|
251
|
+
with.keyword :text do |values|
|
252
|
+
result = values
|
253
|
+
end
|
254
|
+
end
|
255
|
+
assert_equal %w(this is some text), result
|
256
|
+
end
|
257
|
+
|
258
|
+
it "an empty search is successful" do
|
259
|
+
result = nil
|
260
|
+
KeywordSearch.search(%<>) do |with|
|
261
|
+
with.default_keyword :text
|
262
|
+
with.keyword :text do |values|
|
263
|
+
result = values
|
264
|
+
end
|
265
|
+
end
|
266
|
+
assert_nil result
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'a negative search' do
|
270
|
+
result = nil
|
271
|
+
|
272
|
+
KeywordSearch.search(%<-site:google.com>) do |with|
|
273
|
+
with.keyword :site do |values, positive|
|
274
|
+
result = [ values, positive ]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
assert_equal [ [ 'google.com' ], false ], result
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'a positive search' do
|
281
|
+
result = nil
|
282
|
+
|
283
|
+
KeywordSearch.search(%<+site:google.com>) do |with|
|
284
|
+
with.keyword :site do |values, positive|
|
285
|
+
result = [ values, positive ]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
assert_equal [ [ 'google.com' ], true ], result
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'a search with no sign' do
|
292
|
+
result = nil
|
293
|
+
|
294
|
+
KeywordSearch.search(%<site:google.com>) do |with|
|
295
|
+
with.keyword :site do |values, positive|
|
296
|
+
result = [ values, positive ]
|
297
|
+
end
|
298
|
+
end
|
299
|
+
assert_equal [ [ 'google.com' ], true ], result
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'a term should default to positive with no sign' do
|
303
|
+
result = nil
|
304
|
+
|
305
|
+
KeywordSearch.search(%<-site:google.com inurl:atom>) do |with|
|
306
|
+
with.keyword :inurl do |values, positive|
|
307
|
+
result = [ values, positive ]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
assert_equal [ %w(atom), true ], result
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'a negative and positive search to the default keyword' do
|
314
|
+
result = []
|
315
|
+
|
316
|
+
KeywordSearch.search(%<text -google.com search>) do |with|
|
317
|
+
with.default_keyword :text
|
318
|
+
with.keyword :text do |values, positive|
|
319
|
+
result << [ values, positive ]
|
320
|
+
end
|
321
|
+
end
|
322
|
+
assert_equal [ [ %w(text search), true ], [ %w(google.com), false ] ], result
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'a negative search to the default keyword with quotes' do
|
326
|
+
result = []
|
327
|
+
|
328
|
+
KeywordSearch.search(%<-google.com>) do |with|
|
329
|
+
with.default_keyword :text
|
330
|
+
with.keyword :text do |values, positive|
|
331
|
+
result << [ values, positive ]
|
332
|
+
end
|
333
|
+
end
|
334
|
+
assert_equal [ [ %w(google.com), false ] ], result
|
335
|
+
end
|
336
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: keyword_search_yjchen
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bruce Williams
|
8
|
+
- Eric Lindvall
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ~>
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.3'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.3'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
description:
|
43
|
+
email:
|
44
|
+
- brwcodes@gmail.com
|
45
|
+
- eric@sevenscale.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- .gitignore
|
51
|
+
- .travis.yml
|
52
|
+
- Gemfile
|
53
|
+
- LICENSE
|
54
|
+
- Manifest
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- keyword_search.gemspec
|
58
|
+
- lib/keyword_search.rb
|
59
|
+
- lib/keyword_search.rl
|
60
|
+
- lib/keyword_search/definition.rb
|
61
|
+
- lib/keyword_search/version.rb
|
62
|
+
- test/test_keyword_search.rb
|
63
|
+
homepage: http://github.com/bruce/keyword_search
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options:
|
69
|
+
- --charset=UTF-8
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.0.3
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Generic library to parse GMail-style search strings for keyword/value pairs;
|
88
|
+
supports definition of valid keywords and handling of quoted values.
|
89
|
+
test_files:
|
90
|
+
- test/test_keyword_search.rb
|