locatine 0.01084 → 0.01100

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,596 +1,49 @@
1
- require "watir"
2
- require "json"
3
- require "fileutils"
4
- require "chromedriver-helper"
1
+ require 'watir'
2
+ require 'json'
3
+ require 'fileutils'
4
+ require 'chromedriver-helper'
5
+
6
+ # Internal requires
7
+ require 'locatine/public'
8
+ require 'locatine/find_logic'
9
+ require 'locatine/file_work'
10
+ require 'locatine/helpers'
11
+ require 'locatine/xpath_generator'
12
+ require 'locatine/find_by_locator'
13
+ require 'locatine/find_by_magic'
14
+ require 'locatine/dialog_logic'
15
+ require 'locatine/find_by_guess'
16
+ require 'locatine/highlight'
17
+ require 'locatine/data_generate'
18
+ require 'locatine/merge'
19
+ require 'locatine/dialog_actions'
5
20
 
6
21
  module Locatine
7
-
8
22
  ##
9
23
  # Search is the main class of the Locatine
10
24
  #
11
25
  # Locatine can search.
12
26
  class Search
13
-
14
- attr_accessor :data, :depth, :browser, :learn, :json, :stability_limit, :scope
15
-
16
- ##
17
- # Creates a new instance of Search
18
- #
19
- # Params:
20
- # +json+ is the name of file to store//read data. Default => "./Locatine_files/default.json"
21
- #
22
- # +depth+ is the value that shows how many data will be stored for element.
23
- #
24
- # +browser+ is the instance of Watir::Browser. Unless provided it gonna be created with locatine-app onboard.
25
- #
26
- # +learn+ shows will locatine ask for assistance from user or will fail on error. learn is true when LEARN parameter is set in environment.
27
- #
28
- # +stability_limit+ shows max times attribute should be present to consider it trusted.
29
- #
30
- # +scope+ will be used in search (if not provided) defaulkt is "Default"
31
- def initialize(json: "./Locatine_files/default.json",
32
- depth: 3,
33
- browser: nil,
34
- learn: ENV['LEARN'].nil? ? false : true,
35
- stability_limit: 10,
36
- scope: "Default")
37
- if !browser
38
- @browser = Watir::Browser.new(:chrome, switches: ["--load-extension=#{HOME}/app"])
39
- else
40
- @browser = browser
41
- end
42
- @json = json
43
- @folder = File.dirname(@json)
44
- @name = File.basename(@json)
45
- @depth = depth
46
- @data = read_create
47
- @learn = learn
48
- @stability_limit = stability_limit
49
- @scope = scope
50
- end
51
-
52
- ##
53
- # Looking for the element
54
- #
55
- # Params:
56
- #
57
- # +scope+ is a parameter that is used to get information about the element from @data. Default is "Default"
58
- #
59
- # +name+ is a parameter that is used to get information about the element from @data. Must not be nil.
60
- #
61
- # +exact+ if true locatine will be forced to use only basic search. Default is false
62
- #
63
- # +locator+ if not empty it is used for the first attempt to find the element. Default is {}
64
- #
65
- # +vars+ hash of variables that will be used for dynamic attributes. See readme for example
66
- #
67
- # +look_in+ only elements of that kind will be used. Use Watir::Browser methods returning collections (:text_fields, :links, :divs, etc.)
68
- #
69
- # +iframe+ if provided locatine will look for elements inside of it
70
- def find(simple_name = nil,
71
- name: nil,
72
- scope: nil,
73
- exact: false,
74
- locator: {},
75
- vars: {},
76
- look_in: nil,
77
- iframe: nil,
78
- return_locator: false,
79
- collection: false)
80
- name ||= simple_name
81
- raise ArgumentError, ":name should be provided" if !name
82
- @type = look_in
83
- @iframe = iframe
84
- scope = @scope if scope.nil?
85
- scope = "Default" if scope.nil?
86
- result = find_by_locator(locator) if locator != {}
87
- if !result
88
- if @data[scope][name].to_h != {}
89
- result = find_by_data(@data[scope][name], vars)
90
- attributes = generate_data(result, vars) if result
91
- if !result && !exact
92
- result, attributes = find_by_magic(name, scope, @data[scope][name], vars)
93
- end
94
- end
95
- end
96
- result, attributes = ask(scope, name, result, vars) if @learn
97
- raise RuntimeError, "Nothing was found for #{scope} #{name}" if !result && !exact
98
- if result
99
- attributes = generate_data(result, vars) if !attributes
100
- store(attributes, scope, name)
101
- return return_locator ? {xpath: generate_xpath(attributes, vars)} : to_subtype(result, collection)
102
- else
103
- return nil
104
- end
105
- end
106
-
107
- ##
108
- # Find alias with return_locator option enforced
109
- def lctr(*args)
110
- enforce(:return_locator, true, *args)
111
- end
112
-
113
- ##
114
- # Find alias with collection option enforced
115
- def collect(*args)
116
- enforce(:collection, true, *args)
117
- end
118
-
119
- private
120
-
121
- ##
122
- # Reading data from provided file which is set on init of the class instance
123
- #
124
- # If there is no dir or\and file they will be created
125
- def read_create
126
- unless File.directory?(@folder)
127
- FileUtils.mkdir_p(@folder)
128
- end
129
- if File.exists?(@json)
130
- hash = Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = {}}}
131
- return hash.merge(JSON.parse(File.read(@json))["data"])
132
- else
133
- f = File.new(@json, "w")
134
- f.puts '{"data" : {}}'
135
- f.close
136
- return Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = {}}}
137
- end
138
- end
139
-
140
- def enforce(what, value, *args)
141
- if args.last.class == Hash
142
- args.last[what] = value
143
- find(*args)
144
- else
145
- temp = Hash.new
146
- temp[what] = value
147
- args.push(temp)
148
- find(*args)
149
- end
150
- end
151
-
152
- def engine
153
- return (@iframe || @browser)
154
- end
155
-
156
- def collection?(the_class)
157
- case the_class.superclass.to_s
158
- when "Object"
159
- return nil
160
- when "Watir::Element"
161
- return false
162
- when "Watir::ElementCollection"
163
- return true
164
- else
165
- return collection?(the_class.superclass)
166
- end
167
- end
168
-
169
- ##
170
- # Getting all the elements matching a locator
171
- def find_by_locator(locator)
172
- method = @type.nil? ? :elements : @type
173
- results = engine.send(method, locator)
174
- case collection?(results.class)
175
- when nil
176
- @type = nil
177
- raise ArgumentError, "#{method} is not good for :look_in property. Use a method of Watir::Browser that returns a collection (like :divs, :links, etc.)"
178
- when true
179
- begin
180
- results[0].wait_until(timeout: @cold_time) { |el| el.present? }
181
- return results
182
- rescue
183
- return nil
184
- end
185
- when false
186
- begin
187
- warn "#{method} works for :look_in. But it is better to use a method of Watir::Browser that returns a collection (like :divs, :links, etc.)"
188
- results.wait_until(timeout: @cold_time) { |el| el.present? }
189
- the_class = results.class
190
- results = engine.elements(locator).to_a.select{|item| item.to_subtype.class == the_class}
191
- return results
192
- rescue
193
- return nil
194
- end
195
- end
196
- end
197
-
198
- def get_trusted(array)
199
- if array.length > 0
200
- max_stability = (array.max_by {|i| i["stability"].to_i})["stability"].to_i
201
- return (array.select {|i| i["stability"].to_i == max_stability}).uniq
202
- else
203
- return []
204
- end
205
- end
206
-
207
- def generate_xpath(data, vars)
208
- xpath = "[not(@id = 'locatine_magic_div')]"
209
- data.each_pair do |depth, array|
210
- trusted = get_trusted(array)
211
- trusted.each do |hash|
212
- case hash["type"]
213
- when "tag"
214
- xpath = "[self::#{process_string(hash["value"], vars)}]" + xpath
215
- when "text"
216
- xpath = "[contains(text(), '#{process_string(hash["value"], vars)}')]" + xpath
217
- when "attribute"
218
- full_part = "[@*"
219
- hash["name"].split("_").each do |part|
220
- full_part = full_part + "[contains(name(), '#{part}')]"
221
- end
222
- xpath = full_part + "[contains(., '#{process_string(hash["value"], vars)}')]]" + xpath
223
- end
224
- end
225
- xpath = '/*' + xpath
226
- end
227
- xpath = '/' + xpath
228
- return xpath
229
- end
230
-
231
- ##
232
- # Getting all the elements via stored information
233
- def find_by_data(data, vars)
234
- find_by_locator({xpath: generate_xpath(data, vars)})
235
- end
236
-
237
- ##
238
- # Getting all the elements via black magic
239
- def find_by_magic(name, scope, data, vars)
240
- warn "Cannot locate #{name} in #{scope} with usual ways. Trying to use magic"
241
- all = []
242
- timeout = @cold_time
243
- @cold_time = 0
244
- data.each_pair do |depth, array|
245
- trusted = get_trusted(array)
246
- trusted.each do |hash|
247
- case hash["type"]
248
- when "tag"
249
- all = all + find_by_tag(hash, vars, depth).to_a
250
- when "text"
251
- all = all + find_by_text(hash, vars, depth).to_a
252
- when "attribute"
253
- all = all + find_by_attribute(hash, vars, depth).to_a
254
- end
255
- end
256
- end
257
- @cold_time = timeout
258
- raise RuntimeError, "Locatine is unable to find element #{name} in #{scope}" if all.length == 0
259
- # Something esoteric here :)
260
- max = all.count(all.max_by {|i| all.count(i)})
261
- suggestion = (all.select {|i| all.count(i) == max}).uniq
262
- attributes = generate_data(suggestion, vars)
263
- return suggestion, attributes
264
- end
265
-
266
- ##
267
- # Getting elements by attribute
268
- def find_by_attribute(hash, vars, depth = 0)
269
- correction = "/*" * depth.to_i
270
- full_part = "//*[@*"
271
- hash["name"].split("_").each do |part|
272
- full_part = full_part + "[contains(name(), '#{part}')]"
273
- end
274
- xpath = full_part + "[., '#{process_string(hash["value"], vars)}')]]"
275
- find_by_locator(xpath: "#{full_part}[contains(., '#{process_string(hash["value"], vars)}')]]#{correction}[not(@id = 'locatine_magic_div')]")
276
- end
277
-
278
- ##
279
- # Getting elements by tag
280
- def find_by_tag(hash, vars, depth = 0)
281
- correction = "/*" * depth.to_i
282
- find_by_locator(xpath: "//*[self::#{process_string(hash["value"], vars)}')]#{correction}[not(@id = 'locatine_magic_div')]")
283
- end
284
-
285
- ##
286
- # Getting elements by text
287
- def find_by_text(hash, vars, depth = 0)
288
- correction = "/*" * depth.to_i
289
- find_by_locator(xpath: "//*[contains(text(), '#{process_string(hash["value"], vars)}')]#{correction}[not(@id = 'locatine_magic_div')]")
290
- end
291
-
292
- ##
293
- # Setting attribute of locatine div (way to communicate)
294
- def send_to_app(what, value, b = engine)
295
- fix_iframe
296
- b.wd.execute_script(%Q[if (document.getElementById('locatine_magic_div')) {
297
- return document.getElementById('locatine_magic_div').setAttribute("#{what}", "#{value}")}])
298
- fix_iframe
299
- end
300
-
301
- ##
302
- # Getting attribute of locatine div (way to communicate)
303
- def get_from_app(what)
304
- fix_iframe
305
- result = engine.wd.execute_script(%Q[if (document.getElementById('locatine_magic_div')) {
306
- return document.getElementById('locatine_magic_div').getAttribute("#{what}")}])
307
- fix_iframe
308
- return result
309
- end
310
-
311
- def fix_iframe
312
- if @iframe
313
- @iframe = @browser.iframe(@iframe.selector)
314
- end
315
- end
316
-
317
- def set_title(text)
318
- puts text
319
- send_to_app("locatinetitle", text)
320
- end
321
-
322
- ##
323
- # Sending request to locatine app
324
- def start_listening(scope, name)
325
- send_to_app("locatinestyle", "blocked", @browser) if @iframe
326
- send_to_app("locatinehint", "Toggle single//collection mode button if you need. If you want to do some actions on the page toggle Locatine waiting button. You also can select element on devtools -> Elements. Do not forget to confirm your selection.")
327
- send_to_app("locatinestyle", "set_true")
328
- sleep 0.5
329
- end
330
-
331
- def find_by_guess(scope, name, vars)
332
- all = []
333
- timeout = @cold_time
334
- @cold_time = 0
335
- name.split(" ").each do |part|
336
- all = all + find_by_locator({xpath: "//#{part}[not(@id = 'locatine_magic_div')]"}).to_a
337
- all = all + find_by_locator({xpath: "//*[contains(text(),'#{part}')][not(@id = 'locatine_magic_div')]"}).to_a
338
- all = all + find_by_locator({xpath: "//*[@*[contains(., '#{part}')]][not(@id = 'locatine_magic_div')]"}).to_a
339
- end
340
- if all.length>0
341
- max = all.count(all.max_by {|i| all.count(i)})
342
- guess = (all.select {|i| all.count(i) == max}).uniq
343
- guess_data = generate_data(guess, vars)
344
- by_data = find_by_data(guess_data, vars)
345
- if by_data.nil? || (engine.elements.length/by_data.length <=4)
346
- set_title "Locatine has no good guess for #{name} in #{scope}. Try to change the name. Or just define it."
347
- guess = nil
348
- guess_data = {}
349
- end
350
- else
351
- set_title "Locatine has no guess for #{name} in #{scope}. Try to change the name. Or just define it."
352
- end
353
- @cold_time = timeout
354
- return guess, guess_data.to_h
355
- end
356
-
357
- ##
358
- # request send and waiting for an answer
359
- def ask(scope, name, result, vars)
360
- start_listening(scope, name)
361
- element, attributes, finished, old_tag, old_index, old_element = result, {}, false, nil, nil, nil
362
- if !element.nil?
363
- attributes = generate_data(element, vars)
364
- elsif name.length >= 5
365
- set_title("Locatine is trying to guess what is #{name} in #{scope}.")
366
- element, attributes = find_by_guess(scope, name, vars)
367
- end
368
- while !finished do
369
- sleep 0.1
370
- tag = get_from_app("tag")
371
- tag = tag.downcase if !tag.nil?
372
- index = get_from_app("index").to_i
373
- if (!tag.to_s.strip.empty?) && ((tag != old_tag) or (old_index != index))
374
- element = [engine.elements({tag_name: tag})[index]]
375
- new_attributes = generate_data(element, vars)
376
- if get_from_app("locatinecollection") == "true"
377
- attributes = get_commons(new_attributes, attributes)
378
- element = find_by_data(attributes, vars)
379
- else
380
- attributes = new_attributes
381
- end
382
- end
383
- if old_element != element
384
- mass_highlight_turn(old_element, false) if old_element
385
- mass_highlight_turn(element) if element
386
- if element.nil?
387
- set_title "Nothing is selected as #{name} in #{scope}"
388
- else
389
- set_title "#{element.length} elements were selected as #{name} in #{scope}. If it is correct - confirm the selection."
390
- end
391
- end
392
- old_element, old_tag, old_index = element, tag, index
393
- case get_from_app("locatineconfirmed")
394
- when "true"
395
- send_to_app("locatineconfirmed", "ok")
396
- send_to_app("locatinetitle", "Right now you are defining nothing. So no button will work")
397
- send_to_app("locatinehint", "Place for a smart hint here")
398
- finished = true
399
- when "declined"
400
- send_to_app("locatineconfirmed", "ok")
401
- element, old_tag, old_index, tag, index, attributes = nil, nil, nil, nil, nil, {}
402
- end
403
- end
404
- mass_highlight_turn(element, false)
405
- send_to_app("locatinestyle", "ok", @browser) if @iframe
406
- sleep 0.5
407
- return element, attributes
408
- end
409
-
410
- ##
411
- # We can highlight an element
412
- def highlight(element)
413
- if !element.stale? && element.exists?
414
- begin
415
- engine.execute_script("arguments[0].setAttribute"\
416
- "('locatineclass','foundbylocatine')", element)
417
- rescue
418
- warn " something was found as #{element.selector} but we cannot highlight it"
419
- end
420
- end
421
- end
422
-
423
- ##
424
- # We can unhighlight an element
425
- def unhighlight(element)
426
- if !element.stale? && element.exists?
427
- begin
428
- engine.execute_script("arguments[0].removeAttribute('locatineclass')",
429
- element)
430
- rescue
431
- # watir is not allowing to play with attributes of some strange elements
432
- end
433
- end
434
- end
435
-
436
- ##
437
- # We can highlight\unhighlight tons of elements at once
438
- def mass_highlight_turn(mass, turn_on = true)
439
- mass.each do |element|
440
- if turn_on
441
- highlight element
442
- else
443
- unhighlight element
444
- end
445
- end
446
- end
447
-
448
- ##
449
- # Generating array of hashes representing data of the element
450
- def get_element_info(element, vars)
451
- attrs = []
452
- get_attributes(element).each do |hash|
453
- if vars[hash["name"].to_sym]
454
- hash["value"].gsub!(vars[hash["name"].to_sym], "\#{#{hash["name"]}}")
455
- end
456
- attrs.push hash
457
- end
458
- txt = (element.text == element.inner_html) ? element.text : ''
459
- tag = element.tag_name
460
- if vars[:tag] == tag
461
- tag = "\#{tag}"
462
- end
463
- attrs.push({"name" => "tag", "value" => tag, "type" => "tag"})
464
- txt.split(" ").each do |word|
465
- if !vars[:text].to_s.strip.empty?
466
- final_word = word.gsub(vars[:text].to_s, "\#{text}")
467
- else
468
- final_word = word
469
- end
470
- attrs.push({"name" => "text", "value" => final_word, "type" => "text"})
471
- end
472
- return attrs
473
- end
474
-
475
- ##
476
- # Merging data of two elements (new data is to find both)
477
- def get_commons(first, second)
478
- second = first if second == {}
479
- final = Hash.new { |hash, key| hash[key] = [] }
480
- first.each_pair do |depth, array|
481
- array.each do |hash|
482
- to_add = second[depth].select {|item| (item["name"] == hash["name"]) and (item["value"] == hash["value"]) and item["type"] == hash["type"]}
483
- final[depth] = final[depth] + to_add
484
- end
485
- end
486
- final
487
- end
488
-
489
- ##
490
- # Setting stability
491
- def set_stability(first, second)
492
- second = first if second.to_h == {}
493
- final = Hash.new { |hash, key| hash[key] = [] }
494
- first.each_pair do |depth, array|
495
- array.each do |hash|
496
- to_add = second[depth].select {|item| (item["name"] == hash["name"]) and (item["value"] == hash["value"]) and item["type"] == hash["type"]}
497
- if to_add.length > 0 # old ones
498
- to_add[0]["stability"] = (to_add[0]["stability"].to_i + 1).to_s if (to_add[0]["stability"].to_i < @stability_limit)
499
- final[depth] = final[depth] + to_add
500
- else # new ones
501
- hash["stability"] = "1"
502
- final[depth] = final[depth].push hash
503
- end
504
- end
505
- final[depth].uniq!
506
- end
507
- final
508
- end
509
-
510
- ##
511
- # Generating data for group of elements
512
- def generate_data(result, vars)
513
- family = {}
514
- result.each do |item|
515
- family = get_commons(get_family_info(item, vars), family)
516
- end
517
- return family
518
- end
519
-
520
- ##
521
- # Getting element\\parents information
522
- def get_family_info(element, vars)
523
- current_depth = 0
524
- attributes = {};
525
- while current_depth != @depth
526
- attributes[current_depth.to_s] = get_element_info(element, vars)
527
- current_depth = current_depth+1
528
- element = element.parent
529
- # Sometimes watir is not returning a valid parent that's why:
530
- current_depth = @depth if !element.parent.exists?
531
- end
532
- return attributes
533
- end
534
-
535
- ##
536
- # Saving json
537
- def store(attributes, scope, name)
538
- @data[scope][name] = set_stability(attributes, @data[scope][name])
539
- to_write = ({"data" => @data})
540
- File.open(@json, "w") do |f|
541
- f.write(JSON.pretty_generate(to_write))
542
- end
543
- end
544
-
545
- ##
546
- # Collecting attributes of the element
547
- def get_attributes(element)
548
- attributes = element.attributes
549
- array = Array.new
550
- attributes.each_pair do |name, value|
551
- if (name.to_s != "locatineclass")
552
- value.split(" ").uniq.each do |part|
553
- array.push({"name" => name.to_s, "type" => "attribute", "value" => part})
554
- end
555
- end
556
- end
557
- return array
558
- end
559
-
560
- ##
561
- # Replacing dynamic entries with values
562
- def process_string(str, vars)
563
- str ||= ""
564
- n = nil
565
- while str != n
566
- str = n if !n.nil?
567
- thevar = str.match(/\#{[^\#{]*}/).to_s
568
- if thevar != ""
569
- value = vars[thevar.match(/(\w.*)}/)[1].to_sym]
570
- raise ArgumentError, ":#{thevar.match(/(\w.*)}/)[1]} must be provided in vars since element was defined with it" if !value
571
- n = str.gsub(thevar, value)
572
- else
573
- n = str
574
- end
575
- end
576
- str
577
- end
578
-
579
- ##
580
- # Returning subtype of the only element of collection OR collection
581
- #
582
- # Params:
583
- # +result+ must be Watir::HTMLElementCollection or Array
584
- #
585
- # +collection+ nil, true or false
586
- def to_subtype(result, collection)
587
- case collection
588
- when true
589
- return result
590
- when false
591
- return result.first.to_subtype
592
- end
593
- end
27
+ include Locatine::Merge
28
+ include Locatine::Public
29
+ include Locatine::Helpers
30
+ include Locatine::FileWork
31
+ include Locatine::FindLogic
32
+ include Locatine::Highlight
33
+ include Locatine::FindByMagic
34
+ include Locatine::DialogLogic
35
+ include Locatine::FindByGuess
36
+ include Locatine::DataGenerate
37
+ include Locatine::FindByLocator
38
+ include Locatine::DialogActions
39
+ include Locatine::XpathGenerator
40
+
41
+ attr_accessor :data,
42
+ :depth,
43
+ :browser,
44
+ :learn,
45
+ :json,
46
+ :stability_limit,
47
+ :scope
594
48
  end
595
-
596
49
  end
@@ -1,6 +1,11 @@
1
1
  module Locatine
2
2
  # constants here...
3
- VERSION = "0.01084"
4
- NAME = "locatine"
5
- HOME = File.readable?("#{Dir.pwd}/lib/#{Locatine::NAME}")? "#{Dir.pwd}/lib/#{Locatine::NAME}" : "#{Gem.dir}/gems/#{Locatine::NAME}-#{Locatine::VERSION}/lib/#{Locatine::NAME}"
3
+ VERSION = '0.01100'.freeze
4
+ NAME = 'locatine'.freeze
5
+ HOME = if File.readable?("#{Dir.pwd}/lib/#{Locatine::NAME}")
6
+ "#{Dir.pwd}/lib/#{Locatine::NAME}"
7
+ else
8
+ "#{Gem.dir}/gems/#{Locatine::NAME}-#{Locatine::VERSION}/"\
9
+ "lib/#{Locatine::NAME}"
10
+ end
6
11
  end
@@ -0,0 +1,47 @@
1
+ module Locatine
2
+ ##
3
+ # Methods for generation xpath from stored data
4
+ module XpathGenerator
5
+ private
6
+
7
+ def get_trusted(array)
8
+ if !array.empty?
9
+ max_stability = (array.max_by { |i| i['stability'].to_i })['stability']
10
+ (array.select { |i| i['stability'].to_i == max_stability.to_i }).uniq
11
+ else
12
+ []
13
+ end
14
+ end
15
+
16
+ def generate_xpath(data, vars)
17
+ xpath = "[not(@id = 'locatine_magic_div')]"
18
+ data.each_pair do |_depth, array|
19
+ get_trusted(array).each do |hash|
20
+ xpath = generate_xpath_part(hash, vars) + xpath
21
+ end
22
+ xpath = '/*' + xpath
23
+ end
24
+ xpath = '/' + xpath
25
+ xpath
26
+ end
27
+
28
+ def generate_xpath_part(hash, vars)
29
+ value = process_string(hash['value'], vars)
30
+ case hash['type']
31
+ when 'tag'
32
+ "[self::#{value}]"
33
+ when 'text'
34
+ "[contains(text(), '#{value}')]"
35
+ when 'attribute'
36
+ generate_xpath_part_from_attribute(hash, value)
37
+ end
38
+ end
39
+
40
+ def generate_xpath_part_from_attribute(hash, value)
41
+ full = '[@*'
42
+ hash['name'].split('_')
43
+ .each { |part| full += "[contains(name(), '#{part}')]" }
44
+ full + "[contains(., '#{value}')]]"
45
+ end
46
+ end
47
+ end
data/lib/locatine.rb CHANGED
@@ -1,2 +1,2 @@
1
- require "locatine/search"
2
- require "locatine/version"
1
+ require 'locatine/search'
2
+ require 'locatine/version'