sitediff 0.0.2 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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