sitediff 0.0.6 → 1.2.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.
- checksums.yaml +5 -5
- data/.eslintignore +1 -0
- data/.eslintrc.json +28 -0
- data/.project +11 -0
- data/.rubocop.yml +179 -0
- data/.rubocop_todo.yml +51 -0
- data/CHANGELOG.md +28 -0
- data/Dockerfile +33 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +85 -0
- data/INSTALLATION.md +146 -0
- data/LICENSE +339 -0
- data/README.md +810 -0
- data/Rakefile +12 -0
- data/Thorfile +135 -0
- data/bin/sitediff +9 -2
- data/config/.gitkeep +0 -0
- data/config/sanitize_domains.example.yaml +8 -0
- data/config/sitediff.example.yaml +81 -0
- data/docker-compose.test.yml +3 -0
- data/lib/sitediff/api.rb +276 -0
- data/lib/sitediff/cache.rb +57 -8
- data/lib/sitediff/cli.rb +156 -176
- data/lib/sitediff/config/creator.rb +61 -77
- data/lib/sitediff/config/preset.rb +75 -0
- data/lib/sitediff/config.rb +436 -31
- data/lib/sitediff/crawler.rb +27 -21
- data/lib/sitediff/diff.rb +32 -9
- data/lib/sitediff/fetch.rb +10 -3
- data/lib/sitediff/files/diff.html.erb +20 -2
- data/lib/sitediff/files/jquery.min.js +2 -0
- data/lib/sitediff/files/normalize.css +349 -0
- data/lib/sitediff/files/report.html.erb +171 -0
- data/lib/sitediff/files/sidebyside.html.erb +5 -2
- data/lib/sitediff/files/sitediff.css +303 -30
- data/lib/sitediff/files/sitediff.js +367 -0
- data/lib/sitediff/presets/drupal.yaml +63 -0
- data/lib/sitediff/report.rb +254 -0
- data/lib/sitediff/result.rb +50 -20
- data/lib/sitediff/sanitize/dom_transform.rb +47 -8
- data/lib/sitediff/sanitize/regexp.rb +24 -3
- data/lib/sitediff/sanitize.rb +81 -12
- data/lib/sitediff/uriwrapper.rb +65 -23
- data/lib/sitediff/webserver/resultserver.rb +30 -33
- data/lib/sitediff/webserver.rb +15 -3
- data/lib/sitediff.rb +130 -83
- data/misc/sitediff - overview report.png +0 -0
- data/misc/sitediff - page report.png +0 -0
- data/package-lock.json +878 -0
- data/package.json +25 -0
- data/sitediff.gemspec +51 -0
- metadata +91 -29
- data/lib/sitediff/files/html_report.html.erb +0 -66
- data/lib/sitediff/files/rules/drupal.yaml +0 -63
- data/lib/sitediff/rules.rb +0 -65
data/lib/sitediff/sanitize.rb
CHANGED
@@ -8,21 +8,26 @@ require 'nokogiri'
|
|
8
8
|
require 'set'
|
9
9
|
|
10
10
|
class SiteDiff
|
11
|
+
# SiteDiff Sanitizer.
|
11
12
|
class Sanitizer
|
12
13
|
class InvalidSanitization < SiteDiffException; end
|
13
14
|
|
14
15
|
TOOLS = {
|
15
16
|
array: %w[dom_transform sanitization],
|
16
|
-
scalar: %w[selector remove_spacing]
|
17
|
+
scalar: %w[selector remove_spacing ignore_whitespace]
|
17
18
|
}.freeze
|
18
|
-
DOM_TRANSFORMS = Set.new(%w[remove unwrap_root unwrap remove_class])
|
19
|
+
DOM_TRANSFORMS = Set.new(%w[remove strip unwrap_root unwrap remove_class])
|
19
20
|
|
21
|
+
##
|
22
|
+
# Creates a Sanitizer.
|
20
23
|
def initialize(html, config, opts = {})
|
21
24
|
@html = html
|
22
25
|
@config = config
|
23
26
|
@opts = opts
|
24
27
|
end
|
25
28
|
|
29
|
+
##
|
30
|
+
# Performs sanitization.
|
26
31
|
def sanitize
|
27
32
|
return '' if @html == '' # Quick return on empty input
|
28
33
|
|
@@ -30,7 +35,7 @@ class SiteDiff
|
|
30
35
|
@html = nil
|
31
36
|
|
32
37
|
remove_spacing
|
33
|
-
selector
|
38
|
+
regions || selector
|
34
39
|
dom_transforms
|
35
40
|
regexps
|
36
41
|
|
@@ -56,13 +61,13 @@ class SiteDiff
|
|
56
61
|
def canonicalize_rule(name)
|
57
62
|
(rules = @config[name]) || (return nil)
|
58
63
|
|
59
|
-
|
60
|
-
|
64
|
+
# Already an array? Do nothing.
|
65
|
+
if rules[0].respond_to?('each') && rules[0]&.fetch('value')
|
66
|
+
# If it is a hash, put it in an array.
|
61
67
|
elsif rules['value']
|
62
|
-
# Hash, put it in an array
|
63
68
|
rules = [rules]
|
69
|
+
# If it is a scalar value, put it in an array.
|
64
70
|
else
|
65
|
-
# Scalar, put it in a hash
|
66
71
|
rules = [{ 'value' => rules }]
|
67
72
|
end
|
68
73
|
|
@@ -79,6 +84,13 @@ class SiteDiff
|
|
79
84
|
Sanitizer.remove_node_spacing(@node) if rule['value']
|
80
85
|
end
|
81
86
|
|
87
|
+
# Perform 'regions' action, don't perform 'selector' if regions exist.
|
88
|
+
def regions
|
89
|
+
return unless validate_regions
|
90
|
+
|
91
|
+
@node = select_regions(@node, @config['regions'], @opts[:output])
|
92
|
+
end
|
93
|
+
|
82
94
|
# Perform 'selector' action, to choose a new root
|
83
95
|
def selector
|
84
96
|
(rule = canonicalize_rule('selector')) || return
|
@@ -99,7 +111,13 @@ class SiteDiff
|
|
99
111
|
# Prevent potential UTF-8 encoding errors by removing bytes
|
100
112
|
# Not the only solution. An alternative is to return the
|
101
113
|
# string unmodified.
|
102
|
-
@html = @html.encode(
|
114
|
+
@html = @html.encode(
|
115
|
+
'UTF-8',
|
116
|
+
'binary',
|
117
|
+
invalid: :replace,
|
118
|
+
undef: :replace,
|
119
|
+
replace: ''
|
120
|
+
)
|
103
121
|
global.each { |r| r.apply(@html) }
|
104
122
|
end
|
105
123
|
|
@@ -124,6 +142,20 @@ class SiteDiff
|
|
124
142
|
end
|
125
143
|
end
|
126
144
|
|
145
|
+
# Restructure the node into regions.
|
146
|
+
def select_regions(node, regions, output)
|
147
|
+
regions = output.map do |name|
|
148
|
+
selector = get_named_region(regions, name)['selector']
|
149
|
+
region = Nokogiri::XML.fragment("<region id=\"#{name}\"></region>").at_css('region')
|
150
|
+
matching = node.css(selector)
|
151
|
+
matching.each { |m| region.add_child m }
|
152
|
+
region
|
153
|
+
end
|
154
|
+
node = Nokogiri::HTML.fragment('')
|
155
|
+
regions.each { |r| node.add_child r }
|
156
|
+
node
|
157
|
+
end
|
158
|
+
|
127
159
|
# Get a fragment consisting of the elements matching the selector(s)
|
128
160
|
def self.select_fragments(node, sel)
|
129
161
|
# When we choose a new root, we always become a DocumentFragment,
|
@@ -151,7 +183,13 @@ class SiteDiff
|
|
151
183
|
# Prevent potential UTF-8 encoding errors by removing invalid bytes.
|
152
184
|
# Not the only solution.
|
153
185
|
# An alternative is to return the string unmodified.
|
154
|
-
str = str.encode(
|
186
|
+
str = str.encode(
|
187
|
+
'UTF-8',
|
188
|
+
'binary',
|
189
|
+
invalid: :replace,
|
190
|
+
undef: :replace,
|
191
|
+
replace: ''
|
192
|
+
)
|
155
193
|
# Remove xml declaration and <html> tags
|
156
194
|
str.sub!(/\A<\?xml.*$\n/, '')
|
157
195
|
str.sub!(/\A^<html>$\n/, '')
|
@@ -164,11 +202,15 @@ class SiteDiff
|
|
164
202
|
# Remove blank lines
|
165
203
|
str.gsub!(/^\s*$\n/, '')
|
166
204
|
|
205
|
+
# Remove DOS newlines
|
206
|
+
str.gsub!(/\x0D$/, '')
|
207
|
+
str.gsub!(/ $/, '')
|
208
|
+
|
167
209
|
str
|
168
210
|
end
|
169
211
|
|
170
212
|
# Parse HTML into a node
|
171
|
-
def self.domify(str, force_doc
|
213
|
+
def self.domify(str, force_doc: false)
|
172
214
|
if force_doc || /<!DOCTYPE/.match(str[0, 512])
|
173
215
|
Nokogiri::HTML(str)
|
174
216
|
else
|
@@ -182,10 +224,37 @@ class SiteDiff
|
|
182
224
|
obj
|
183
225
|
# node or fragment
|
184
226
|
elsif Nokogiri::XML::Node == obj.class || Nokogiri::HTML::DocumentFragment == obj.class
|
185
|
-
domify(obj.to_s, true)
|
227
|
+
domify(obj.to_s, force_doc: true)
|
186
228
|
else
|
187
|
-
to_document(domify(obj, false))
|
229
|
+
to_document(domify(obj, force_doc: false))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
# Validate `regions` and `output` from config.
|
236
|
+
def validate_regions
|
237
|
+
return false unless @config['regions'].is_a?(Array)
|
238
|
+
|
239
|
+
return false unless @opts[:output].is_a?(Array)
|
240
|
+
|
241
|
+
regions = @config['regions']
|
242
|
+
output = @opts[:output]
|
243
|
+
regions.each do |region|
|
244
|
+
return false unless region.key?('name') && region.key?('selector')
|
245
|
+
end
|
246
|
+
|
247
|
+
# Check that each named output has an associated region.
|
248
|
+
output.each do |name|
|
249
|
+
return false unless get_named_region(regions, name)
|
188
250
|
end
|
251
|
+
|
252
|
+
true
|
253
|
+
end
|
254
|
+
|
255
|
+
# Return the selector from a named region.
|
256
|
+
def get_named_region(regions, name)
|
257
|
+
regions.find { |region| region['name'] == name }
|
189
258
|
end
|
190
259
|
end
|
191
260
|
end
|
data/lib/sitediff/uriwrapper.rb
CHANGED
@@ -7,19 +7,28 @@ require 'addressable/uri'
|
|
7
7
|
class SiteDiff
|
8
8
|
class SiteDiffReadFailure < SiteDiffException; end
|
9
9
|
|
10
|
+
# SiteDiff URI Wrapper.
|
10
11
|
class UriWrapper
|
12
|
+
# TODO: Move these CURL OPTS to Config.DEFAULT_CONFIG.
|
11
13
|
DEFAULT_CURL_OPTS = {
|
12
|
-
|
13
|
-
|
14
|
+
# Don't hang on servers that don't exist.
|
15
|
+
connecttimeout: 3,
|
16
|
+
# Follow HTTP redirects (code 301 and 302).
|
17
|
+
followlocation: true,
|
14
18
|
headers: {
|
15
19
|
'User-Agent' => 'Sitediff - https://github.com/evolvingweb/sitediff'
|
16
|
-
}
|
20
|
+
},
|
21
|
+
# always accept SSL certs
|
22
|
+
ssl_verifypeer: false,
|
23
|
+
ssl_verifyhost: 0
|
17
24
|
}.freeze
|
18
25
|
|
19
26
|
# This lets us treat errors or content as one object
|
20
27
|
class ReadResult
|
21
28
|
attr_accessor :encoding, :content, :error_code, :error
|
22
29
|
|
30
|
+
##
|
31
|
+
# Creates a ReadResult.
|
23
32
|
def initialize(content = nil, encoding = 'utf-8')
|
24
33
|
@content = content
|
25
34
|
@encoding = encoding
|
@@ -27,15 +36,19 @@ class SiteDiff
|
|
27
36
|
@error_code = nil
|
28
37
|
end
|
29
38
|
|
30
|
-
|
39
|
+
##
|
40
|
+
# Creates a ReadResult with an error.
|
41
|
+
def self.error(message, code = nil)
|
31
42
|
res = new
|
32
43
|
res.error_code = code
|
33
|
-
res.error =
|
44
|
+
res.error = message
|
34
45
|
res
|
35
46
|
end
|
36
47
|
end
|
37
48
|
|
38
|
-
|
49
|
+
##
|
50
|
+
# Creates a UriWrapper.
|
51
|
+
def initialize(uri, curl_opts = DEFAULT_CURL_OPTS, debug: true)
|
39
52
|
@uri = uri.respond_to?(:scheme) ? uri : Addressable::URI.parse(uri)
|
40
53
|
# remove trailing '/'s from local URIs
|
41
54
|
@uri.path.gsub!(%r{/*$}, '') if local?
|
@@ -43,14 +56,20 @@ class SiteDiff
|
|
43
56
|
@debug = debug
|
44
57
|
end
|
45
58
|
|
59
|
+
##
|
60
|
+
# Returns the "user" part of the URI.
|
46
61
|
def user
|
47
62
|
@uri.user
|
48
63
|
end
|
49
64
|
|
65
|
+
##
|
66
|
+
# Returns the "password" part of the URI.
|
50
67
|
def password
|
51
68
|
@uri.password
|
52
69
|
end
|
53
70
|
|
71
|
+
##
|
72
|
+
# Converts the URI to a string.
|
54
73
|
def to_s
|
55
74
|
uri = @uri.dup
|
56
75
|
uri.user = nil
|
@@ -58,19 +77,22 @@ class SiteDiff
|
|
58
77
|
uri.to_s
|
59
78
|
end
|
60
79
|
|
80
|
+
##
|
61
81
|
# Is this a local filesystem path?
|
62
82
|
def local?
|
63
83
|
@uri.scheme.nil?
|
64
84
|
end
|
65
85
|
|
86
|
+
## What does this one do?
|
66
87
|
# FIXME: this is not used anymore
|
67
|
-
def +(
|
88
|
+
def +(other)
|
68
89
|
# 'path' for SiteDiff includes (parts of) path, query, and fragment.
|
69
90
|
sep = ''
|
70
91
|
sep = '/' if local? || @uri.path.empty?
|
71
|
-
self.class.new(@uri.to_s + sep +
|
92
|
+
self.class.new(@uri.to_s + sep + other)
|
72
93
|
end
|
73
94
|
|
95
|
+
##
|
74
96
|
# Reads a file and yields to the completion handler, see .queue()
|
75
97
|
def read_file
|
76
98
|
File.open(@uri.to_s, 'r:UTF-8') { |f| yield ReadResult.new(f.read) }
|
@@ -81,10 +103,9 @@ class SiteDiff
|
|
81
103
|
# Returns the encoding of an HTTP response from headers , nil if not
|
82
104
|
# specified.
|
83
105
|
def charset_encoding(http_headers)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
106
|
+
content_type = http_headers['Content-Type']
|
107
|
+
if (md = /;\s*charset=([-\w]*)/.match(content_type))
|
108
|
+
md[1]
|
88
109
|
end
|
89
110
|
end
|
90
111
|
|
@@ -95,7 +116,7 @@ class SiteDiff
|
|
95
116
|
def typhoeus_request
|
96
117
|
params = @curl_opts.dup
|
97
118
|
# Allow basic auth
|
98
|
-
params[:userpwd] = @uri.user
|
119
|
+
params[:userpwd] = "#{@uri.user}: #{@uri.password}" if @uri.user
|
99
120
|
|
100
121
|
req = Typhoeus::Request.new(to_s, params)
|
101
122
|
|
@@ -114,25 +135,34 @@ class SiteDiff
|
|
114
135
|
rescue ArgumentError => e
|
115
136
|
raise if @debug
|
116
137
|
|
117
|
-
yield ReadResult.error(
|
118
|
-
|
138
|
+
yield ReadResult.error(
|
139
|
+
"Parsing error for #{@uri}: #{e.message}"
|
140
|
+
)
|
141
|
+
rescue StandardError => e
|
119
142
|
raise if @debug
|
120
143
|
|
121
|
-
yield ReadResult.error(
|
144
|
+
yield ReadResult.error(
|
145
|
+
"Unknown parsing error for #{@uri}: #{e.message}"
|
146
|
+
)
|
122
147
|
end
|
123
148
|
end
|
124
149
|
|
125
150
|
req.on_failure do |resp|
|
126
151
|
if resp&.status_message
|
127
|
-
|
128
|
-
|
129
|
-
|
152
|
+
yield ReadResult.error(
|
153
|
+
"HTTP error when loading #{@uri} : [#{resp.response_code}] #{resp.status_message}",
|
154
|
+
resp.response_code
|
155
|
+
)
|
130
156
|
elsif (msg = resp.options[:return_code])
|
131
|
-
yield ReadResult.error(
|
132
|
-
|
157
|
+
yield ReadResult.error(
|
158
|
+
"Connection error when loading #{@uri} : [#{resp.options[:return_code]}] #{resp.status_message} #{msg}",
|
159
|
+
resp.response_code
|
160
|
+
)
|
133
161
|
else
|
134
|
-
yield ReadResult.error(
|
135
|
-
|
162
|
+
yield ReadResult.error(
|
163
|
+
"Unknown error when loading #{@uri} : [#{resp.response_code}] #{resp.status_message}",
|
164
|
+
resp.response_code
|
165
|
+
)
|
136
166
|
end
|
137
167
|
end
|
138
168
|
|
@@ -152,5 +182,17 @@ class SiteDiff
|
|
152
182
|
hydra.queue(typhoeus_request(&handler))
|
153
183
|
end
|
154
184
|
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Canonicalize a path.
|
188
|
+
#
|
189
|
+
# @param [String] path
|
190
|
+
# A base relative path. Example: /foo/bar
|
191
|
+
def self.canonicalize(path)
|
192
|
+
# Ignore trailing slashes for all paths except "/" (front page).
|
193
|
+
path = path.chomp('/') unless path == '/'
|
194
|
+
# If the path is empty, assume that it's the front page.
|
195
|
+
path.empty? ? '/' : path
|
196
|
+
end
|
155
197
|
end
|
156
198
|
end
|
@@ -1,18 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'sitediff'
|
4
|
+
require 'sitediff/report'
|
4
5
|
require 'sitediff/webserver'
|
5
6
|
require 'erb'
|
6
7
|
|
7
8
|
class SiteDiff
|
8
9
|
class Webserver
|
10
|
+
# SiteDiff Result Server.
|
9
11
|
class ResultServer < Webserver
|
10
12
|
# Display a page from the cache
|
11
13
|
class CacheServlet < WEBrick::HTTPServlet::AbstractServlet
|
14
|
+
##
|
15
|
+
# Creates a Cache Servlet.
|
12
16
|
def initialize(_server, cache)
|
17
|
+
super
|
13
18
|
@cache = cache
|
14
19
|
end
|
15
20
|
|
21
|
+
##
|
22
|
+
# Performs a GET request.
|
16
23
|
def do_GET(req, res)
|
17
24
|
path = req.path_info
|
18
25
|
(md = %r{^/([^/]+)(/.*)$}.match(path)) ||
|
@@ -29,13 +36,19 @@ class SiteDiff
|
|
29
36
|
end
|
30
37
|
end
|
31
38
|
|
32
|
-
|
39
|
+
##
|
40
|
+
# Display two pages side by side.
|
33
41
|
class SideBySideServlet < WEBrick::HTTPServlet::AbstractServlet
|
42
|
+
##
|
43
|
+
# Creates a Side By Side Servlet.
|
34
44
|
def initialize(_server, cache, settings)
|
45
|
+
super
|
35
46
|
@cache = cache
|
36
47
|
@settings = settings
|
37
48
|
end
|
38
49
|
|
50
|
+
##
|
51
|
+
# Generates URLs for a given path.
|
39
52
|
def urls(path)
|
40
53
|
%w[before after].map do |tag|
|
41
54
|
base = @settings[tag]
|
@@ -44,6 +57,8 @@ class SiteDiff
|
|
44
57
|
end
|
45
58
|
end
|
46
59
|
|
60
|
+
##
|
61
|
+
# Performs a GET request.
|
47
62
|
def do_GET(req, res)
|
48
63
|
path = req.path_info
|
49
64
|
before, after = *urls(path)
|
@@ -54,52 +69,29 @@ class SiteDiff
|
|
54
69
|
end
|
55
70
|
end
|
56
71
|
|
57
|
-
|
58
|
-
|
59
|
-
def initialize(_server, dir)
|
60
|
-
@dir = dir
|
61
|
-
end
|
62
|
-
|
63
|
-
def do_GET(req, res)
|
64
|
-
path = req.path_info
|
65
|
-
if path != '/diff'
|
66
|
-
res['content-type'] = 'text/plain'
|
67
|
-
res.body = 'ERROR: Only /run/diff is supported by the /run API at the moment'
|
68
|
-
return
|
69
|
-
end
|
70
|
-
# Thor assumes only one command is called and some values like
|
71
|
-
# `options` are share across all SiteDiff::Cli instances so
|
72
|
-
# we can't just call SiteDiff::Cli.new().diff
|
73
|
-
# This is likely to go very wrong depending on how `sitediff serve`
|
74
|
-
# was actually called
|
75
|
-
cmd = "#{$PROGRAM_NAME} diff -C #{@dir} --cached=all"
|
76
|
-
system(cmd)
|
77
|
-
|
78
|
-
# Could also add a message to indicate success/failure
|
79
|
-
# But for the moment, all our files are static
|
80
|
-
res.set_redirect(WEBrick::HTTPStatus::Found,
|
81
|
-
"/files/#{SiteDiff::REPORT_FILE}")
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
72
|
+
##
|
73
|
+
# Creates a Result Server.
|
85
74
|
def initialize(port, dir, opts = {})
|
86
|
-
unless File.exist?(File.join(dir,
|
75
|
+
unless File.exist?(File.join(dir, Report::SETTINGS_FILE))
|
87
76
|
raise SiteDiffException,
|
88
77
|
"Please run 'sitediff diff' before running 'sitediff serve'"
|
89
78
|
end
|
90
79
|
|
91
|
-
@settings = YAML.load_file(File.join(dir,
|
80
|
+
@settings = YAML.load_file(File.join(dir, Report::SETTINGS_FILE))
|
81
|
+
puts @settings
|
92
82
|
@cache = opts[:cache]
|
93
83
|
super(port, [dir], opts)
|
94
84
|
end
|
95
85
|
|
86
|
+
##
|
87
|
+
# TODO: Document what this method does.
|
96
88
|
def server(opts)
|
97
89
|
dir = opts.delete(:DocumentRoot)
|
98
90
|
srv = super(opts)
|
99
91
|
srv.mount_proc('/') do |req, res|
|
100
92
|
if req.path == '/'
|
101
93
|
res.set_redirect(WEBrick::HTTPStatus::Found,
|
102
|
-
"/files/#{
|
94
|
+
"/files/#{Report::REPORT_FILE_HTML}")
|
103
95
|
else
|
104
96
|
res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect,
|
105
97
|
"#{@settings['after']}#{req.path}")
|
@@ -109,10 +101,11 @@ class SiteDiff
|
|
109
101
|
srv.mount('/files', WEBrick::HTTPServlet::FileHandler, dir, true)
|
110
102
|
srv.mount('/cache', CacheServlet, @cache)
|
111
103
|
srv.mount('/sidebyside', SideBySideServlet, @cache, @settings)
|
112
|
-
srv.mount('/run', RunServlet, dir)
|
113
104
|
srv
|
114
105
|
end
|
115
106
|
|
107
|
+
##
|
108
|
+
# Sets up the server.
|
116
109
|
def setup
|
117
110
|
super
|
118
111
|
root = uris.first
|
@@ -120,6 +113,8 @@ class SiteDiff
|
|
120
113
|
open_in_browser(root) if @opts[:browse]
|
121
114
|
end
|
122
115
|
|
116
|
+
##
|
117
|
+
# Opens a URL in a browser.
|
123
118
|
def open_in_browser(url)
|
124
119
|
commands = %w[xdg-open open]
|
125
120
|
cmd = commands.find { |c| which(c) }
|
@@ -127,6 +122,8 @@ class SiteDiff
|
|
127
122
|
cmd
|
128
123
|
end
|
129
124
|
|
125
|
+
##
|
126
|
+
# TODO: Document what this method does.
|
130
127
|
def which(cmd)
|
131
128
|
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
132
129
|
file = File.join(path, cmd)
|
data/lib/sitediff/webserver.rb
CHANGED
@@ -3,13 +3,15 @@
|
|
3
3
|
require 'webrick'
|
4
4
|
|
5
5
|
class SiteDiff
|
6
|
+
# SiteDiff Web Server.
|
6
7
|
class Webserver
|
7
|
-
# Simple
|
8
|
+
# Simple web server for testing purposes.
|
8
9
|
DEFAULT_PORT = 13_080
|
9
10
|
|
10
11
|
attr_accessor :ports
|
11
12
|
|
12
|
-
|
13
|
+
##
|
14
|
+
# Serve a list of directories.
|
13
15
|
def initialize(start_port, dirs, opts = {})
|
14
16
|
start_port ||= DEFAULT_PORT
|
15
17
|
@ports = (start_port...(start_port + dirs.size)).to_a
|
@@ -25,14 +27,20 @@ class SiteDiff
|
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
30
|
+
##
|
31
|
+
# Kills the server.
|
28
32
|
def kill
|
29
33
|
@threads.each(&:kill)
|
30
34
|
end
|
31
35
|
|
36
|
+
##
|
37
|
+
# Waits for the server.
|
32
38
|
def wait
|
33
39
|
@threads.each(&:join)
|
34
40
|
end
|
35
41
|
|
42
|
+
##
|
43
|
+
# Maps URIs to defined ports and returns a list of URIs.
|
36
44
|
def uris
|
37
45
|
ports.map { |p| "http://localhost:#{p}" }
|
38
46
|
end
|
@@ -63,20 +71,24 @@ class SiteDiff
|
|
63
71
|
|
64
72
|
public
|
65
73
|
|
74
|
+
# SiteDiff Fixture Server.
|
66
75
|
class FixtureServer < Webserver
|
67
76
|
PORT = DEFAULT_PORT + 1
|
68
|
-
BASE = 'spec/
|
77
|
+
BASE = 'spec/sites/ruby-doc.org'
|
69
78
|
NAMES = %w[core-1.9.3 core-2.0].freeze
|
70
79
|
|
80
|
+
# Initialize web server.
|
71
81
|
def initialize(port = PORT, base = BASE, names = NAMES)
|
72
82
|
dirs = names.map { |n| File.join(base, n) }
|
73
83
|
super(port, dirs, quiet: true)
|
74
84
|
end
|
75
85
|
|
86
|
+
# Get the before site uri.
|
76
87
|
def before
|
77
88
|
uris.first
|
78
89
|
end
|
79
90
|
|
91
|
+
# Get the after site uri.
|
80
92
|
def after
|
81
93
|
uris.last
|
82
94
|
end
|