digger 0.1.3 → 0.1.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 26deb68788670976da4b7edc2170236d35e695b5
4
- data.tar.gz: 8e1c83697d912e8b401c4933c8d7e437024559c8
2
+ SHA256:
3
+ metadata.gz: 2f506fd615df8b9d732d6b67bedc72644df26dc3f5725cfe5dba10c1098bae0b
4
+ data.tar.gz: 0e9afcb19ba0be5ce4a90787d54c3b43d58100bac3ec45bf5ef93781a60b6eb1
5
5
  SHA512:
6
- metadata.gz: 47d08104b079873296e1c94cdb6ecd384be7e02e38343eba91c25dbfa27be067bd62084d7347b4c9dd0662ea17cbbacfa3196c6f2bcbf2a18e4919996b613e91
7
- data.tar.gz: b29784d26b7044e6201ad9322fa932d7226ac68bffc2bcf6f802dee98f79097ed88c2e0c4dbe0bc4e99fd5e4dc4c7bf0a6562d25687b616477973f90bd91f514
6
+ metadata.gz: 8608a2ee8e06ddd846772d40dc3e417560229729b7b736eda8a7f50977a7d2c6fc523f86fe64480b5172c5295137eae7c85f945f7ca310502c54c8b90dd75e8d
7
+ data.tar.gz: 59fef1a13adc8f983c16428ee4d08d6ccdecea09b7583452b6ca07689727c3d6a386f5a5b767a20d117c8c4bc9775a2a6b32fd4caff1c1a5e85ee2af82e39d1b
data/digger.gemspec CHANGED
@@ -18,8 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.7"
22
- spec.add_development_dependency "rake", "~> 10.0"
21
+ spec.add_development_dependency "bundler", "~> 2.0"
22
+ spec.add_development_dependency "rake", ">= 12.3.3"
23
23
 
24
24
  spec.add_runtime_dependency 'nokogiri', '~> 1.6'
25
25
  spec.add_runtime_dependency 'http-cookie', '~> 1.0'
data/lib/digger/http.rb CHANGED
@@ -49,7 +49,7 @@ module Digger
49
49
  url = URI(url)
50
50
  pages = []
51
51
  get(url, referer) do |response, code, location, redirect_to, response_time|
52
- handle_compression response
52
+ handle_compression response if handle_compression?
53
53
  pages << Page.new(location, body: response.body,
54
54
  code: code,
55
55
  headers: response.to_hash,
@@ -70,6 +70,13 @@ module Digger
70
70
  [Page.new(url, error: e, referer: referer, depth: depth)]
71
71
  end
72
72
 
73
+ #
74
+ # Accept response compression, may bring encoding error if true
75
+ #
76
+ def handle_compression?
77
+ @opts[:handle_compression]
78
+ end
79
+
73
80
  #
74
81
  # The maximum number of redirects to follow
75
82
  #
@@ -185,7 +192,7 @@ module Digger
185
192
  opts['User-Agent'] = user_agent if user_agent
186
193
  opts['Referer'] = referer.to_s if referer
187
194
  opts['Cookie'] = ::HTTP::Cookie.cookie_value(cookie_jar.cookies(url)) if accept_cookies?
188
- opts['Accept-Encoding'] = 'gzip,deflate'
195
+ opts['Accept-Encoding'] = 'gzip,deflate' if handle_compression?
189
196
 
190
197
  retries = 0
191
198
  begin
data/lib/digger/index.rb CHANGED
@@ -8,33 +8,34 @@ module Digger
8
8
 
9
9
  def urls
10
10
  @urls ||= begin
11
- args = self.args.map{|a| (a.respond_to? :each) ? a.to_a : [a]}
12
- args.shift.product(*args).map{|arg| pattern_applied_url(arg)}
11
+ args = self.args.map { |a| a.respond_to?(:each) ? a.to_a : [a] }
12
+ args.shift.product(*args).map { |arg| pattern_applied_url(arg) }
13
13
  end
14
14
  end
15
15
 
16
16
  def pattern_applied_url(arg)
17
- pattern.gsub('*').each_with_index{|_, i| arg[i]}
17
+ pattern.gsub('*').each_with_index { |_, i| arg[i] }
18
18
  end
19
19
 
20
20
  def self.batch(entities, cocurrence = 1, &block)
21
- raise NoBlockError, "No block given" unless block
21
+ raise NoBlockError, 'No block given' unless block
22
22
 
23
23
  if cocurrence > 1
24
- results = {}
25
- entities.each_slice(cocurrence) do |group|
24
+ results = Array.new(entities.size)
25
+ entities.each_slice(cocurrence).with_index do |group, idx1|
26
26
  threads = []
27
- group.each do |entity|
27
+ group.each_with_index do |entity, idx2|
28
+ index = idx1 * cocurrence + idx2
28
29
  threads << Thread.new(entity) do |ent|
29
- results[ent] = block.call(ent)
30
+ results[index] = block.call(ent)
30
31
  end
31
32
  end
32
- threads.each{|thread| thread.join}
33
+ threads.each(&:join)
33
34
  end
34
- entities.map{|ent| results[ent]}
35
+ results
35
36
  else
36
- entities.map{|ent| block.call(ent) }
37
+ entities.map { |ent| block.call(ent) }
37
38
  end
38
39
  end
39
40
  end
40
- end
41
+ end
data/lib/digger/model.rb CHANGED
@@ -1,16 +1,19 @@
1
1
 
2
2
  module Digger
3
3
  class Model
4
- @@digger_config = {'pattern'=>{}, 'index'=>{}}
4
+ @@digger_config = {
5
+ 'pattern' => {},
6
+ 'index' => {}
7
+ }
5
8
 
6
9
  class << self
7
10
  # patterns
8
11
  def pattern_config
9
- @@digger_config['pattern'][self.name] ||= {}
12
+ @@digger_config['pattern'][name] ||= {}
10
13
  end
11
14
 
12
15
  Pattern::TYPES.each do |method|
13
- define_method method, ->(pairs, &block){
16
+ define_method method, -> (pairs, &block) {
14
17
  pairs.each_pair do |key, value|
15
18
  pattern_config[key] = Pattern.new(type: method, value: value, block: block)
16
19
  end
@@ -18,21 +21,22 @@ module Digger
18
21
  end
19
22
 
20
23
  def validate_presence(*keys)
21
- keys_all = pattern_config.keys
22
- raise "Pattern keys #{(keys - keys_all).join(', ')} should be present" unless keys.all?{|k| keys_all.include?(k) }
24
+ is_all = pattern_config.keys.all? { |k| keys.include?(k) }
25
+ raise "Pattern keys #{(keys - keys_all).join(', ')} should be present" unless is_all
23
26
  end
24
27
 
25
28
  def validate_includeness(*keys)
26
- raise "Pattern keys #{(pattern_config.keys - keys).join(', ')} should not be included" unless pattern_config.keys.all?{|k| keys.include?(k)}
29
+ is_all = pattern_config.keys.all? { |k| keys.include?(k) }
30
+ raise "Pattern keys #{(pattern_config.keys - keys).join(', ')} should not be included" if is_all
27
31
  end
28
32
 
29
33
  # index page
30
34
  def index_config
31
- @@digger_config['index'][self.name]
35
+ @@digger_config['index'][name]
32
36
  end
33
37
 
34
38
  def index_page(pattern, *args)
35
- @@digger_config['index'][self.name] = Index.new(pattern, args)
39
+ @@digger_config['index'][name] = Index.new(pattern, args)
36
40
  end
37
41
 
38
42
  def index_page?
@@ -55,13 +59,15 @@ module Digger
55
59
  end
56
60
 
57
61
  def dig_urls(urls, cocurrence = 1, opts = {})
58
- Index.batch(urls, cocurrence){|url| dig_url(url, opts) }
62
+ Index.batch(urls, cocurrence) { |url| dig_url(url, opts) }
59
63
  end
60
64
 
61
65
  def dig(cocurrence = 1)
62
66
  if self.class.index_page?
63
- self.class.index_config.process(cocurrence){|url| dig_url(url) }
67
+ self.class.index_config.process(cocurrence) do |url|
68
+ dig_url(url)
69
+ end
64
70
  end
65
71
  end
66
72
  end
67
- end
73
+ end
data/lib/digger/page.rb CHANGED
@@ -3,6 +3,7 @@ require 'json'
3
3
  require 'ostruct'
4
4
  require 'set'
5
5
  require 'kconv'
6
+ require 'uri'
6
7
 
7
8
  # https://github.com/taganaka/polipus/blob/master/lib/polipus/page.rb
8
9
  module Digger
@@ -27,16 +28,12 @@ module Digger
27
28
  # OpenStruct it holds users defined data
28
29
  attr_accessor :user_data
29
30
 
30
- attr_accessor :aliases
31
-
32
- attr_accessor :domain_aliases
31
+ attr_accessor :aliases, :domain_aliases, :fetched_at
33
32
 
34
33
  # Whether the current page should be stored
35
34
  # Default: true
36
35
  attr_accessor :storable
37
36
 
38
- attr_accessor :fetched_at
39
-
40
37
  #
41
38
  # Create a new page
42
39
  #
@@ -60,7 +57,7 @@ module Digger
60
57
  end
61
58
 
62
59
  def title
63
- doc.title if doc
60
+ doc&.title
64
61
  end
65
62
 
66
63
  #
@@ -74,6 +71,7 @@ module Digger
74
71
  doc.search('//a[@href]').each do |a|
75
72
  u = a['href']
76
73
  next if u.nil? || u.empty?
74
+
77
75
  abs = to_absolute(u) rescue next
78
76
  @links << abs if abs && in_domain?(abs)
79
77
  end
@@ -95,7 +93,13 @@ module Digger
95
93
  end
96
94
  end
97
95
 
96
+ def json
97
+ @json ||= JSON.parse body
98
+ end
98
99
 
100
+ def jsonp
101
+ @jsonp ||= JSON.parse body.match(/^[^(]+?\((.+)\)[^)]*$/)[1]
102
+ end
99
103
 
100
104
  #
101
105
  # Discard links, a next call of page.links will return an empty array
@@ -156,7 +160,7 @@ module Digger
156
160
  # returns +false+ otherwise.
157
161
  #
158
162
  def not_found?
159
- 404 == @code
163
+ @code == 404
160
164
  end
161
165
 
162
166
  #
@@ -170,6 +174,7 @@ module Digger
170
174
  end unless @base
171
175
 
172
176
  return nil if @base && @base.to_s.empty?
177
+
173
178
  @base
174
179
  end
175
180
 
@@ -180,16 +185,7 @@ module Digger
180
185
  def to_absolute(link)
181
186
  return nil if link.nil?
182
187
 
183
- # link = link.to_s.encode('utf-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '')
184
-
185
- # remove anchor
186
- link =
187
- begin
188
- URI.encode(URI.decode(link.to_s.gsub(/#[a-zA-Z0-9_-]*$/, '')))
189
- rescue URI::Error
190
- return nil
191
- end
192
-
188
+ link = link.to_s.encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(/#[\w]*$/, '')
193
189
  relative = begin
194
190
  URI(link)
195
191
  rescue URI::Error
@@ -247,6 +243,7 @@ module Digger
247
243
 
248
244
  def expired?(ttl)
249
245
  return false if fetched_at.nil?
246
+
250
247
  (Time.now.to_i - ttl) > fetched_at
251
248
  end
252
249
 
@@ -1,91 +1,113 @@
1
1
  require 'nokogiri'
2
2
 
3
3
  module Digger
4
+ # Extractor patterns definition
4
5
  class Pattern
5
6
  attr_accessor :type, :value, :block
6
7
 
7
8
  def initialize(hash = {})
8
- hash.each_pair{|key, value| send("#{key}=", value) if %w{type value block}.include?(key.to_s)}
9
+ hash.each_pair { |key, value| send("#{key}=", value) if %w[type value block].include?(key.to_s)}
9
10
  end
10
11
 
11
- def safe_block
12
- block && begin
13
- if block.respond_to?(:call)
14
- block
15
- elsif block.strip == '' #
16
- nil
17
- else
18
- proc{ $SAFE = 2; eval block }.call
19
- end
20
- rescue StandardError
21
- nil
12
+ def safe_block(&default_block)
13
+ if block.nil? || (block.is_a?(String) && block.strip.empty?)
14
+ default_block
15
+ elsif block.respond_to?(:call)
16
+ block
17
+ else
18
+ proc {
19
+ $SAFE = 2
20
+ eval block
21
+ }.call
22
22
  end
23
23
  end
24
24
 
25
25
  def self.wrap(hash)
26
- Hash[hash.map{|key, value| [key, value.is_a?(Pattern) ? value : Pattern.new(value)]}]
26
+ hash.transform_values { |value| value.is_a?(Pattern) ? value : Pattern.new(value) }
27
27
  end
28
28
 
29
29
  MATCH_MAX = 3
30
-
31
- TYPES = 0.upto(MATCH_MAX).map{|i| "match_#{i}"} + %w{match_many css_one css_many}
32
30
 
33
- def regexp?
34
- TYPES.index(type) <= MATCH_MAX + 1 # match_many in addition
31
+ TYPES_REGEXP = 0.upto(MATCH_MAX).map { |i| "match_#{i}" } + %w[match_many]
32
+ TYPES_CSS = %w[css_one css_many].freeze
33
+ TYPES_JSON = %w[json jsonp].freeze
34
+
35
+ TYPES = TYPES_REGEXP + TYPES_CSS + TYPES_JSON
36
+
37
+ def match_page(page)
38
+ return unless page.success?
39
+ if TYPES_REGEXP.include?(type) # regular expression
40
+ regexp_match(page.body)
41
+ elsif TYPES_CSS.include?(type) # css expression
42
+ css_match(page.doc)
43
+ elsif TYPES_JSON.include?(type)
44
+ json_match(page)
45
+ end
35
46
  end
36
47
 
37
- def match_page(page, &callback)
38
- blk = callback || safe_block
39
- if regexp? # regular expression
40
- index = TYPES.index(type)
41
- blk ||= ->(text){text.strip}
42
- # content is String
43
- if type == 'match_many'
44
- match = page.body.gsub(value).to_a
45
- else
46
- matches = page.body.match(value)
47
- match = matches.nil? ? nil : matches[index]
48
- end
49
- else # css expression
50
- blk ||= ->(node){node.content.strip}
51
- # content is Nokogiri::HTML::Document
52
- if type == 'css_one'
53
- match = page.doc.css(value).first
54
- elsif type == 'css_many' # css_many
55
- match = page.doc.css(value)
56
- end
48
+ def json_match(page)
49
+ block = safe_block { |j| j }
50
+ json = page.send(type)
51
+ keys = json_index_keys(value)
52
+ match = json_fetch(json, keys)
53
+ block.call(match)
54
+ end
55
+
56
+ def css_match(doc)
57
+ block = safe_block { |node| node.content.strip }
58
+ # content is Nokogiri::HTML::Document
59
+ contents = doc.css(value)
60
+ if type == 'css_many'
61
+ contents.map { |node| block.call(node) }.uniq
62
+ else
63
+ block.call(contents.first)
64
+ end
65
+ end
66
+
67
+ def regexp_match(body)
68
+ block = safe_block(&:strip)
69
+ # content is String
70
+ if type == 'match_many'
71
+ body.gsub(value).to_a.map { |node| block.call(node) }.uniq
72
+ else
73
+ index = TYPES_REGEXP.index(type)
74
+ matches = body.match(value)
75
+ block.call(matches[index]) unless matches.nil?
57
76
  end
58
- if match.nil?
59
- nil
60
- elsif %w{css_many match_many}.include? type
61
- match.map{|node| blk.call(node) }.uniq
77
+ end
78
+
79
+ def json_fetch(json, keys)
80
+ if keys.empty?
81
+ json
62
82
  else
63
- blk.call(match)
83
+ pt = keys.shift
84
+ json_fetch(json[pt[:index] || pt[:key]], keys)
64
85
  end
65
- rescue
66
- nil
67
86
  end
68
87
 
88
+ def json_index_keys(keys)
89
+ keys.to_s.match(/^\$\S*$/)[0].scan(/(\.(\w+)|\[(\d+)\])/).map do |p|
90
+ p[1].nil? ? { index: p[2].to_i } : { key: p[1] }
91
+ end
92
+ end
93
+
94
+ private :json_index_keys, :json_fetch
95
+
96
+ # Nokogiri node methods
69
97
  class Nokogiri::XML::Node
70
- %w{one many}.each do |name|
71
- define_method "inner_#{name}" do |css, &block|
72
- callback = ->(node) do
73
- if node
74
- (block || ->(n){n.text.strip}).call(node)
75
- else
76
- nil
77
- end
78
- end
98
+ %w[one many].each do |name|
99
+ define_method "inner_#{name}" do |css, &block|
100
+ callback = ->(node) { (block || ->(n) { n.text.strip }).call(node) if node }
79
101
  if name == 'one' # inner_one
80
102
  callback.call(self.css(css).first)
81
103
  else # inner_many
82
- self.css(css).map{|node| callback.call(node)}
104
+ self.css(css).map { |node| callback.call(node) }
83
105
  end
84
106
  end
85
107
  end
86
108
  def source
87
109
  to_xml
88
110
  end
89
- end # nokogiri
111
+ end
90
112
  end
91
- end
113
+ end
@@ -1,3 +1,3 @@
1
1
  module Digger
2
- VERSION = "0.1.3"
2
+ VERSION = '0.1.7'.freeze
3
3
  end
data/spec/digger_spec.rb CHANGED
@@ -1,15 +1,14 @@
1
1
  require 'digger'
2
2
 
3
3
  http = Digger::HTTP.new
4
- page = http.fetch_page('http://nan.so/')
4
+ page = http.fetch_page('http://www.baidu.com/')
5
5
 
6
- pattern = Digger::Pattern.new({type: 'css_many', value: '.sites>a>span' })
6
+ pattern = Digger::Pattern.new({ type: 'css_many', value: '#s-top-left>a' })
7
7
 
8
8
  class Item < Digger::Model
9
- css_many sites: '.sites>a>span'
10
- css_one logo: '.logo'
9
+ css_many sites: '#s-top-left>a'
11
10
  validate_presence :sites
12
- validate_includeness :sites, :logo
11
+ validate_includeness :sites
13
12
  end
14
13
 
15
14
  describe Digger do
@@ -19,12 +18,12 @@ describe Digger do
19
18
 
20
19
  it "pattern should match content" do
21
20
  sites = pattern.match_page(page)
22
- expect(sites.include?('百度网盘')).to eq(true)
21
+ expect(sites.include?('新闻')).to eq(true)
23
22
  end
24
23
 
25
24
  it "model should dig content" do
26
25
  item = Item.new.match_page(page)
27
- expect(item[:sites].include?('读远')).to be(true)
26
+ expect(item[:sites].include?('新闻')).to be(true)
28
27
  end
29
28
 
30
29
  it "validation support" do
@@ -0,0 +1,12 @@
1
+ require 'digger'
2
+
3
+ describe Digger::Index do
4
+ it 'batch digger' do
5
+ list = [1, 2, 3, 4, 5, 6, 7, 8]
6
+ pt = Digger::Index.batch(list, 3) do |num|
7
+ sleep(rand(1..3))
8
+ "##{num}"
9
+ end
10
+ expect(pt.join).to eq(list.map { |num| "##{num}" }.join)
11
+ end
12
+ end
data/spec/page_spec.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'digger'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ describe Digger::Page do
6
+ it 'page json' do
7
+ json_str = '{"a":1,"b":[1,2,3]}'
8
+ j1 = Digger::Page.new('', body: json_str)
9
+ j2 = Digger::Page.new('', body: "hello(#{json_str});")
10
+ expect(j1.json['a']).to eq(1)
11
+ expect(j2.jsonp['a']).to eq(1)
12
+ expect(j1.json['b'][0]).to eq(1)
13
+ expect(j2.jsonp['b'][1]).to eq(2)
14
+ end
15
+
16
+ it 'fetch baidu' do
17
+ http = Digger::HTTP.new
18
+ page = http.fetch_page('http://www.baidu.com/')
19
+ expect(page.code).to eq(200)
20
+ end
21
+
22
+ it 'page uri' do
23
+ link ='https://www.baidu.com/s?wd=%E5%93%88%E5%93%88#hello'
24
+ link = link.to_s.encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(/#[\w]*$/, '')
25
+ p link
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ require 'digger'
2
+ require 'json'
3
+
4
+ describe Digger::Pattern do
5
+ it 'json fetch' do
6
+ json = JSON.parse('[{"a":1,"b":[1,2,3]}]')
7
+ pt = Digger::Pattern.new
8
+ expect(pt.json_fetch(json, '$[0]')['a']).to eq(1)
9
+ expect(pt.json_fetch(json, '$[0].a')).to eq(1)
10
+ expect(pt.json_fetch(json, '$[0].b').length).to eq(3)
11
+ expect(pt.json_fetch(json, '$[0].b[2]')).to eq(3)
12
+ end
13
+
14
+
15
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: digger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - binz
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-22 00:00:00.000000000 Z
11
+ date: 2021-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: 12.3.3
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: 12.3.3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: nokogiri
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -87,12 +87,15 @@ files:
87
87
  - lib/digger/pattern.rb
88
88
  - lib/digger/version.rb
89
89
  - spec/digger_spec.rb
90
+ - spec/index_spec.rb
91
+ - spec/page_spec.rb
92
+ - spec/pattern_spec.rb
90
93
  - spec/validate_spec.rb
91
94
  homepage: ''
92
95
  licenses:
93
96
  - MIT
94
97
  metadata: {}
95
- post_install_message:
98
+ post_install_message:
96
99
  rdoc_options: []
97
100
  require_paths:
98
101
  - lib
@@ -107,11 +110,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
110
  - !ruby/object:Gem::Version
108
111
  version: '0'
109
112
  requirements: []
110
- rubyforge_project:
111
- rubygems_version: 2.2.2
112
- signing_key:
113
+ rubygems_version: 3.2.32
114
+ signing_key:
113
115
  specification_version: 4
114
116
  summary: Dig need stractual infomation from web page.
115
117
  test_files:
116
118
  - spec/digger_spec.rb
119
+ - spec/index_spec.rb
120
+ - spec/page_spec.rb
121
+ - spec/pattern_spec.rb
117
122
  - spec/validate_spec.rb