sitediff 0.0.2 → 1.1.1

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.
@@ -0,0 +1,367 @@
1
+ /**
2
+ * @file
3
+ * SiteDiff report behaviors.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /* global $ */
9
+ /**
10
+ * SiteDiff namespace.
11
+ */
12
+ var SiteDiff = SiteDiff || {};
13
+
14
+ /**
15
+ * SiteDiff global map of diffs.
16
+ */
17
+ SiteDiff.diffs = SiteDiff.diffs || {};
18
+
19
+ SiteDiff.currentDiff = -1;
20
+
21
+ /**
22
+ * Scrolls the document to the said position.
23
+ *
24
+ * @param options
25
+ * Object specifying various options.
26
+ *
27
+ * x: X position.
28
+ * y: Y position.
29
+ * animate: Whether to animate.
30
+ * callback: A function to call after scrolling.
31
+ */
32
+ SiteDiff.scrollToPosition = function (options) {
33
+ // Compute vertical and horizontal adjustments, if any.
34
+ // Example: Fixed elements, etc.
35
+ var xFix = 0;
36
+ var yFix = 0 - 100;
37
+
38
+ // Determine final x and y offsets.
39
+ var x = parseInt(options.x) + xFix;
40
+ x = Math.max(x, 0);
41
+ var y = parseInt(options.y) + yFix;
42
+ y = Math.max(y, 0);
43
+
44
+ // Perform the scroll with or without animation.
45
+ window.scrollTo(x, y);
46
+
47
+ // Trigger a callback, if any.
48
+ if (options.callback) {
49
+ options.callback();
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Scrolls to a DOM element on the page.
55
+ *
56
+ * @param el
57
+ * The DOM element.
58
+ *
59
+ * @param options
60
+ * Object specifying various options.
61
+ *
62
+ * "callback" to trigger after scrolling.
63
+ */
64
+ SiteDiff.scrollToElement = function (el, options) {
65
+ options = options || {};
66
+ var callback = options.callback || function () {};
67
+
68
+ // See if the element exists.
69
+ var $el = $(el).first();
70
+ if ($el.length == 1) {
71
+ // Inject callback to focus on the element we scroll to.
72
+ options.x = 0;
73
+ options.y = $el.offset().top;
74
+ options.callback = function () {
75
+ $el.focus();
76
+ callback.call(el);
77
+ };
78
+ SiteDiff.scrollToPosition(options);
79
+ }
80
+ };
81
+
82
+ /**
83
+ * Initialize behaviors.
84
+ */
85
+ SiteDiff.init = function () {
86
+ // On the overview page.
87
+ switch ($(document.body).data('page')) {
88
+ case 'overview':
89
+ SiteDiff.initFilterForm();
90
+ break;
91
+
92
+ case 'diff':
93
+ SiteDiff.jumpToFirstDiff();
94
+ break;
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Initializes report filters and overlay.
100
+ */
101
+ SiteDiff.initFilterForm = function () {
102
+ SiteDiff.initDiffArray();
103
+ SiteDiff.initStatusFilter();
104
+ SiteDiff.initSearchFilter();
105
+ SiteDiff.initOverlay();
106
+ SiteDiff.initClickHandlers();
107
+ };
108
+
109
+ /**
110
+ * Initialize global diff array
111
+ *
112
+ */
113
+ SiteDiff.initDiffArray = function() {
114
+ SiteDiff.diffs = $('.button-diff').map(function (i, element) {
115
+ var $el = $(element);
116
+ $el.data('diffindex', i);
117
+ return {
118
+ diff: $el.attr('href'),
119
+ element: $el,
120
+ index: i,
121
+ path: $el.parents('.description').find('.path').text()
122
+ };
123
+ });
124
+ };
125
+
126
+ /**
127
+ * Initializes the "status" filter.
128
+ */
129
+ SiteDiff.initStatusFilter = function () {
130
+ $('.form-item--status input')
131
+ .on('change', function () {
132
+ // Get a list of applied filters.
133
+ var appliedFilters = $('.form-item--status input:checked')
134
+ .map(function () {
135
+ return this.getAttribute('value');
136
+ // applied.push(this.getAttribute('value'));
137
+ });
138
+ // Show only matching results, hide the rest.
139
+ $('#sitediff-report')
140
+ .find('.sitediff-result')
141
+ .each(function () {
142
+ var $row = $(this);
143
+ var status = $row.data('status');
144
+ if (
145
+ // Row matches applied filters.
146
+ $.inArray(status, appliedFilters) > -1 ||
147
+ // No filters are applied.
148
+ appliedFilters.length === 0
149
+ ) {
150
+ $row.removeAttr('hidden');
151
+ }
152
+ else {
153
+ $row.attr('hidden', 'hidden');
154
+ }
155
+ });
156
+ });
157
+ };
158
+
159
+ /**
160
+ * Initializes the "search" filter.
161
+ */
162
+ SiteDiff.initSearchFilter = function () {
163
+ $('#input-search')
164
+ .on('change keyup', function () {
165
+ var keyword = $(this).val().toLowerCase();
166
+
167
+ // Filter the records.
168
+ // TODO: Trigger one event per 250ms.
169
+ $('#sitediff-report')
170
+ .find('.sitediff-result')
171
+ .each(function () {
172
+ var $row = $(this);
173
+ var path = $row.find('.path').text();
174
+
175
+ // If keyword matches, keep the row visible.
176
+ if (path.toLowerCase().indexOf(keyword) > -1) {
177
+ $row.attr('hidden', null);
178
+ }
179
+ else {
180
+ $row.attr('hidden', 'hidden');
181
+ }
182
+ });
183
+ });
184
+ };
185
+
186
+ /**
187
+ * Set up the diff overlay to be displayed.
188
+ */
189
+ SiteDiff.initOverlay = function () {
190
+ if (SiteDiff.diffs.length <= 0) return;
191
+
192
+ // add overlay
193
+ $('body').append($(
194
+ '<div class="overlay" style="display: none;"><div class="overlay__inner"><header>' +
195
+ '<div class="path"></div>' +
196
+ '<div class="prev"><a href="#" title="Previous diff (left arrow)">< Prev</a></div>' +
197
+ '<div class="next"><a href="#" title="Next diff (right arrow)">Next ></a></div>' +
198
+ '<div class="exit"><a href="#" title="Close diff display (Esc)">Close</a></div>' +
199
+ '</header><article></article></div></div>'));
200
+ // add header click handlers
201
+ $('.overlay header .exit').click(function (event) {
202
+ event.preventDefault();
203
+ SiteDiff.destroyOverlay();
204
+ });
205
+ $('.overlay header .prev').click(function (event) {
206
+ event.preventDefault();
207
+ SiteDiff.prevDiff();
208
+ });
209
+ $('.overlay header .next').click(function (event) {
210
+ event.preventDefault();
211
+ SiteDiff.nextDiff();
212
+ });
213
+
214
+ };
215
+
216
+ /**
217
+ * Set up click handlers for all diff buttons
218
+ */
219
+ SiteDiff.initClickHandlers = function () {
220
+ SiteDiff.diffs.each( function (i, diff) {
221
+ diff.element.click({index: i}, function (event) {
222
+ event.preventDefault();
223
+ SiteDiff.openOverlay(event.data.index);
224
+ });
225
+ });
226
+ };
227
+
228
+ /**
229
+ * Set up key handlers for overlay.
230
+ */
231
+ SiteDiff.initKeyHandlers = function () {
232
+ $(document).keyup(function (event) {
233
+ switch (event.which) {
234
+ case 37:
235
+ SiteDiff.prevDiff();
236
+ break;
237
+ case 39:
238
+ SiteDiff.nextDiff();
239
+ break;
240
+ case 27:
241
+ SiteDiff.destroyOverlay();
242
+ break;
243
+ }
244
+ });
245
+ };
246
+
247
+ /**
248
+ * Remove overlay key handlers.
249
+ */
250
+ SiteDiff.removeKeyHandlers = function () {
251
+ $(document).off('keyup');
252
+ };
253
+
254
+ /**
255
+ * Open overlay for the diff identified by the `index`.
256
+ *
257
+ * @param integer index
258
+ * The index of the diff to be viewed.
259
+ */
260
+ SiteDiff.openOverlay = function (index) {
261
+ SiteDiff.currentDiff = index;
262
+ SiteDiff.showDiff();
263
+ SiteDiff.initKeyHandlers();
264
+ $('.overlay').fadeIn(300);
265
+ };
266
+
267
+ /**
268
+ * Create the iframe to display the current diff.
269
+ */
270
+ SiteDiff.showDiff = function () {
271
+ var diff = SiteDiff.diffs[SiteDiff.currentDiff];
272
+ var iframe = '<iframe src="' + diff.diff + '"></iframe>';
273
+ $('.overlay header .path').text(diff.path);
274
+ SiteDiff.setPrevNext();
275
+ $('.overlay article').html(iframe);
276
+ };
277
+
278
+ /**
279
+ * Hide the overlay and clean up.
280
+ */
281
+ SiteDiff.destroyOverlay = function () {
282
+ $('.overlay article').empty();
283
+ SiteDiff.removeKeyHandlers();
284
+ $('.overlay').fadeOut(300, SiteDiff.scrollToButton);
285
+ };
286
+
287
+ /**
288
+ * Display the previous diff.
289
+ */
290
+ SiteDiff.prevDiff = function () {
291
+ if (SiteDiff.currentDiff > 0) {
292
+ SiteDiff.currentDiff--;
293
+ SiteDiff.showDiff();
294
+ }
295
+ };
296
+
297
+ /**
298
+ * Display the next diff.
299
+ */
300
+ SiteDiff.nextDiff = function () {
301
+ if (SiteDiff.currentDiff < SiteDiff.diffs.length - 1) {
302
+ SiteDiff.currentDiff++;
303
+ SiteDiff.showDiff();
304
+ }
305
+ };
306
+
307
+ /**
308
+ * Enable or disable prev and next buttons based on current diff.
309
+ */
310
+ SiteDiff.setPrevNext = function () {
311
+ if (SiteDiff.currentDiff <= 0) {
312
+ // set prev disabled
313
+ $('.overlay header .prev').addClass('disabled');
314
+ }
315
+ else {
316
+ $('.overlay header .prev.disabled').removeClass('disabled');
317
+ }
318
+ if (SiteDiff.currentDiff >= SiteDiff.diffs.length - 1) {
319
+ // set next disabled
320
+ $('.overlay header .next').addClass('disabled');
321
+ }
322
+ else {
323
+ $('.overlay header .next.disabled').removeClass('disabled');
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Scroll to the button associated with the current diff.
329
+ */
330
+ SiteDiff.scrollToButton = function () {
331
+ var $diffButton = SiteDiff.diffs[SiteDiff.currentDiff].element;
332
+ if (! SiteDiff.isElementVisible($diffButton)) {
333
+ SiteDiff.scrollToElement($diffButton);
334
+ }
335
+ };
336
+
337
+ /**
338
+ * Check if an element is at least partly visible.
339
+ * @param element
340
+ */
341
+ SiteDiff.isElementVisible = function (element) {
342
+ var topVisible = $(window).scrollTop();
343
+ var bottomVisible = topVisible + $(window).height();
344
+ var elemTop = $(element).offset().top;
345
+ var elemBottom = elemTop + $(element).height();
346
+ return ((elemBottom <= bottomVisible) && (elemTop >= topVisible));
347
+ };
348
+
349
+ /**
350
+ * Jumps to the first diff on the page.
351
+ */
352
+ SiteDiff.jumpToFirstDiff = function () {
353
+ // Get the first diff hunk.
354
+ var $diff = $('#diff-container')
355
+ .find('.del, .ins')
356
+ .first();
357
+ if ($diff.length === 0) {
358
+ return;
359
+ }
360
+
361
+ // Scroll the window to it!
362
+ setTimeout(function () {
363
+ SiteDiff.scrollToElement($diff[0]);
364
+ }, 250);
365
+ };
366
+
367
+ $(document).ready(SiteDiff.init);
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'minitar'
6
+ require 'sitediff'
7
+ require 'sitediff/config'
8
+ require 'zlib'
9
+
10
+ class SiteDiff
11
+ ##
12
+ # SiteDiff Report Helper.
13
+ class Report
14
+ attr_reader :results, :cache
15
+
16
+ ##
17
+ # Directory where diffs will be generated.
18
+ DIFFS_DIR = 'diffs'
19
+
20
+ ##
21
+ # Name of file containing a list of pages with diffs.
22
+ FAILURES_FILE = 'failures.txt'
23
+
24
+ ##
25
+ # Name of file containing HTML report of diffs.
26
+ REPORT_FILE_HTML = 'report.html'
27
+
28
+ ##
29
+ # Name of file containing JSON report of diffs.
30
+ REPORT_FILE_JSON = 'report.json'
31
+
32
+ ##
33
+ # Name of file containing exported file archive.
34
+ REPORT_FILE_TAR = 'report.tgz'
35
+
36
+ ##
37
+ # Name of directory in which to build the portable report.
38
+ REPORT_BUILD_DIR = '_tmp_report'
39
+
40
+ ##
41
+ # Name of the portable report directory.
42
+ REPORT_DIR = 'report'
43
+
44
+ ##
45
+ # Path to settings used for report.
46
+ SETTINGS_FILE = 'settings.yaml'
47
+
48
+ ##
49
+ # Creates a Reporter object.
50
+ #
51
+ # @param [Config] config.
52
+ # @param [Cache] cache.
53
+ # @param [Array] results.
54
+ def initialize(config, cache, results)
55
+ @config = config
56
+ @cache = cache
57
+ @results = results
58
+ end
59
+
60
+ ##
61
+ # Generates an HTML report.
62
+ #
63
+ # @param [String] dir
64
+ # The directory in which the report is to be generated.
65
+ def generate_html(
66
+ dir,
67
+ report_before = nil,
68
+ report_after = nil
69
+ )
70
+ report_before ||= @config.before_url
71
+ report_after ||= @config.after_url
72
+ @config.before_time = get_timestamp(:before)
73
+ @config.after_time = get_timestamp(:after)
74
+
75
+ dir = SiteDiff.ensure_dir dir
76
+
77
+ write_diffs dir
78
+ write_failures dir
79
+
80
+ # Prepare report.
81
+ report = Diff.generate_html(
82
+ @results,
83
+ report_before,
84
+ report_after,
85
+ @cache,
86
+ @config
87
+ )
88
+
89
+ # Write report.
90
+ report_file = dir + REPORT_FILE_HTML
91
+ report_file.unlink if report_file.file?
92
+ report_file.open('w') { |f| f.write(report) }
93
+
94
+ write_settings dir, report_before, report_after
95
+
96
+ if @config.export
97
+ package_report(dir)
98
+ else
99
+ SiteDiff.log 'Report generated to ' + report_file.expand_path.to_s
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Generates a JSON report.
105
+ #
106
+ # @param dir
107
+ # The directory in which the report is to be generated.
108
+ def generate_json(dir)
109
+ dir = SiteDiff.ensure_dir dir
110
+ write_diffs dir
111
+ write_failures dir
112
+
113
+ # Prepare report.
114
+ report = {
115
+ paths_compared: @results.length,
116
+ paths_diffs: 0,
117
+ paths: {}
118
+ }
119
+ @results.each do |item|
120
+ report[:paths_diffs] += 1 unless item.success?
121
+
122
+ item_report = {
123
+ path: item.path,
124
+ status: item.status,
125
+ message: item.error
126
+ }
127
+ report[:paths][item.path] = item_report
128
+ end
129
+ report = JSON report
130
+
131
+ # Write report.
132
+ report_file = dir + REPORT_FILE_JSON
133
+ report_file.unlink if report_file.file?
134
+ report_file.open('w') { |f| f.write(report) }
135
+
136
+ write_settings dir
137
+
138
+ SiteDiff.log 'Report generated to ' + report_file.expand_path.to_s
139
+ end
140
+
141
+ ##
142
+ # Package report for export.
143
+ def package_report(dir)
144
+ # Create temporaryreport directories.
145
+ temp_path = dir + REPORT_BUILD_DIR
146
+ temp_path.rmtree if temp_path.directory?
147
+ temp_path.mkpath
148
+ report_path = temp_path + REPORT_DIR
149
+ report_path.mkpath
150
+ files_path = report_path + 'files'
151
+ files_path.mkpath
152
+ diffs_path = dir + DIFFS_DIR
153
+
154
+ # Move files to place.
155
+ FileUtils.move(dir + REPORT_FILE_HTML, report_path)
156
+ FileUtils.move(diffs_path, files_path) if diffs_path.directory?
157
+
158
+ # Make tar file.
159
+ Dir.chdir(temp_path) do
160
+ Minitar.pack(
161
+ REPORT_DIR,
162
+ Zlib::GzipWriter.new(File.open(REPORT_FILE_TAR, 'wb'))
163
+ )
164
+ end
165
+ FileUtils.move(temp_path + REPORT_FILE_TAR, dir)
166
+ temp_path.rmtree
167
+ SiteDiff.log 'Archived report generated to ' + dir.join(REPORT_FILE_TAR).to_s
168
+ end
169
+
170
+ ##
171
+ # Creates diff files in a directory named "diffs".
172
+ #
173
+ # If "dir" is /foo/bar, then diffs will be placed in /foo/bar/diffs.
174
+ #
175
+ # @param [Pathname] dir
176
+ # The directory in which a "diffs" directory is to be generated.
177
+ def write_diffs(dir)
178
+ raise Exception 'dir must be a Pathname' unless dir.is_a? Pathname
179
+
180
+ # Delete existing "diffs" dir, if exists.
181
+ diff_dir = dir + DIFFS_DIR
182
+ diff_dir.rmtree if diff_dir.exist?
183
+
184
+ # Write diffs to the diff directory.
185
+ @results.each { |r| r.dump(dir, @config.export) if r.status == Result::STATUS_FAILURE }
186
+ SiteDiff.log "All diff files written to #{diff_dir.expand_path}" unless @config.export
187
+ end
188
+
189
+ ##
190
+ # Writes paths with diffs into a file.
191
+ #
192
+ # @param [Pathname] dir
193
+ # The directory in which the report is to be generated.
194
+ def write_failures(dir)
195
+ raise Exception 'dir must be a Pathname' unless dir.is_a? Pathname
196
+
197
+ failures = dir + FAILURES_FILE
198
+ SiteDiff.log "All failures written to #{failures.expand_path}"
199
+ failures.open('w') do |f|
200
+ @results.each { |r| f.puts r.path unless r.success? }
201
+ end
202
+ end
203
+
204
+ ##
205
+ # Creates report settings.yaml file.
206
+ #
207
+ # TODO: Find a way to avoid having to create this file.
208
+ #
209
+ # @param [Pathname] dir
210
+ # The directory in which the report is to be generated.
211
+ def write_settings(dir, report_before = nil, report_after = nil)
212
+ raise Exception 'dir must be a Pathname' unless dir.is_a? Pathname
213
+
214
+ settings = {
215
+ 'before' => report_before,
216
+ 'after' => report_after,
217
+ 'cached' => %w[before after]
218
+ }
219
+ dir.+(SETTINGS_FILE).open('w') { |f| YAML.dump(settings, f) }
220
+ end
221
+
222
+ ##
223
+ # Returns CSS for HTML report.
224
+ def self.css
225
+ output = ''
226
+ output += File.read(File.join(SiteDiff::FILES_DIR, 'normalize.css'))
227
+ output += File.read(File.join(SiteDiff::FILES_DIR, 'sitediff.css'))
228
+ output
229
+ end
230
+
231
+ ##
232
+ # Returns JS for HTML report.
233
+ def self.js
234
+ output = ''
235
+ output += File.read(File.join(SiteDiff::FILES_DIR, 'jquery.min.js'))
236
+ output += File.read(File.join(SiteDiff::FILES_DIR, 'sitediff.js'))
237
+ output
238
+ end
239
+
240
+ private
241
+
242
+ # Get crawl timestamps
243
+ def get_timestamp(tag)
244
+ timestamp_file = File.join(@config.directory, 'snapshot', tag.to_s, SiteDiff::Cache::TIMESTAMP_FILE)
245
+ if File.exist? timestamp_file
246
+ file = File::Stat.new(timestamp_file)
247
+ time = file.mtime
248
+ time.class == Time ? time.strftime('%Y-%m-%d %H:%M') : ''
249
+ else
250
+ 'unknown'
251
+ end
252
+ end
253
+ end
254
+ end