marmara 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: caffd5bb8874fcb945d1afd57aa80bf8f214783a
4
+ data.tar.gz: cd9f49013b3b591c9620d1193cb801832375c7d3
5
+ SHA512:
6
+ metadata.gz: a6d5ce21997d98f2fc262af141e2bcdc233a03da0c4a23f670f06d0c4d13cb53beb4a831a26e27e2059474ddf4791498721cba33929949e4af094fb1f09344c0
7
+ data.tar.gz: f34584cfe0a76c92472e4c14d34e537323165b2bf3107f5e5f0b824e06d665c9ce59dbb246e3a501f59e00af1856c24ff504cb2db3ccec0b9074d1a834a00e98
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Michael Godwin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Marmara
2
+ Marmara is a Ruby Gem that analyses your css during UI testing and generates a code coverage report.
3
+
4
+ ![Alt text](https://i.imgur.com/zX9SSuF.png)
5
+
6
+ ## Why is CSS code coverage important?
7
+ CSS code coverage is a little different than traditional code coverage.
8
+
9
+ ### Discovering Unused CSS
10
+ Removing dead CSS code will decrease the number size of your CSS that gets delivered to your user and decreases the number amount of work that client will need to perform once it receives that file, both should lead to an faster website overall and may save you server costs.
11
+
12
+ While this tool will tell you which CSS rules were untouched during testing, you shouldn't always consider a deeper analysis before modifying your source. Imagine that you have a report that looks ike the following:
13
+
14
+ ```diff
15
+ /*
16
+ * Make all links red
17
+ */
18
+ + a {
19
+ + color: red;
20
+ + opacity: 0.9;
21
+ + }
22
+
23
+ /*
24
+ * We used to colour our links blue, maybe we should remove this rule...
25
+ */
26
+ - a.my-old-style {
27
+ - color: blue;
28
+ - }
29
+
30
+ /*
31
+ * Do some old IE fixing
32
+ */
33
+ - html.ie-9 a {
34
+ - filter: alpha(opacity=90);
35
+ - }
36
+ ```
37
+
38
+ As you can see here, the first `a` rule was used, so we definitely want to keep it but there is an older rule `a.my-old-style` which can probably be safely removed. The last rule however is a fix for older browsers, so we should probably consider keeping it.
39
+
40
+ ### Safer CSS Refactoring
41
+ Sometimes our CSS files become monoliths when it would be much better to split up a file into smaller modules. By running a subset of your tests, you can safely determine where files can be split.
42
+
43
+ ### Discovering Untested Features
44
+ With traditional code coverage, this is the most important factor in improving your code base, with CSS testing it is still important but to a lesser degree. If you look back at the first example, the fact that these rules are not covered may mean that you are actually not testing important features. You may want to consider adding tests for IE by using a different user agent or setting the html class programmatically.
45
+
46
+ ## Set up
47
+ This project has yet only been set up in a Rails Capybara/Poltergeist environment, more work may need to be done to get it woking in other environment.
48
+
49
+ It is important to run `Marmara.start_recording` before you run any tests and `Marmara.stop_recording` after testing is complete but currently the call to `Marmara.stop_recording` needs to happen before poltergeist as closed its connection with phantomjs. It would probably be best for us to spin up our own process to avoid this.
50
+
51
+ ### 1. Create a Rake task
52
+
53
+ I'm using Cucumber, so I added a new rake task that looks like this:
54
+
55
+ ```ruby
56
+ task "css:cover" do
57
+ Marmara.start_recording
58
+ Rake::Task[:cucumber].execute
59
+ end
60
+ ```
61
+
62
+ ### 2. Capture your output
63
+
64
+ ```ruby
65
+ AfterStep do
66
+ Marmara.record(page) if Marmara.recording?
67
+ end
68
+ ```
69
+
70
+ Since I also want to capture used selectors for mobile, my after step looks more like this:
71
+
72
+ ```ruby
73
+ AfterStep do
74
+ if Marmara.recording?
75
+ Marmara.record(page)
76
+ old_size = page.driver.browser.client.window_size
77
+ page.driver.resize_window(600, 400)
78
+ Marmara.record(page)
79
+ page.driver.resize_window(*old_size)
80
+ end
81
+ end
82
+ ```
83
+
84
+ ### 3. Stop recording and generate your output
85
+
86
+ ```ruby
87
+ at_exit do
88
+ Marmara.stop_recording if Marmara.recording?
89
+ end
90
+ ```
91
+
92
+ ## Development plan
93
+ This project is currently in a "works in my project" stage, there's still work to be done to make it more vendable so I welcome pull requests. In addition, there are probably a lot of tweaks required to the parser and a lot of features that would be nice to have.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ $:.unshift File.expand_path("../lib", __FILE__)
7
+
8
+ require "rspec/core/rake_task"
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ task default: :spec
@@ -0,0 +1,81 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5
+ <link href="https://fonts.googleapis.com/css?family=Cutive+Mono" rel="stylesheet">
6
+ <style type="text/css">
7
+ body {
8
+ margin: 0;
9
+ font-size: 18px;
10
+ background-color: #FBF3E9;
11
+ }
12
+ #code {
13
+ white-space: nowrap;
14
+ }
15
+ #lines {
16
+ position: relative;
17
+ float: left;
18
+ padding: 0 0.75em 0 0.5em;
19
+ text-align: right;
20
+ font-weight: bold;
21
+ color: rgba(0, 0, 0, 0.125);
22
+ letter-spacing: -0.125em;
23
+ transition: color 150ms ease-in-out;
24
+ cursor: default;
25
+ }
26
+ #lines:hover, #lines a:target {
27
+ color: rgba(0, 0, 0, 0.75);
28
+ }
29
+ #lines a {
30
+ color: inherit;
31
+ text-decoration: none;
32
+ display: block;
33
+ }
34
+ #lines a:hover::after, #lines a:target::after {
35
+ content: '';
36
+ position: absolute;
37
+ left: 0;
38
+ width: 100vw;
39
+ background-color: #00BFFF;
40
+ height: 1.6em;
41
+ opacity: 0.25;
42
+ }
43
+ #lines a:hover::after {
44
+ background-color: #BDB76B;
45
+ }
46
+ pre {
47
+ font-family: 'Cutive Mono', monospace;
48
+ display: inline;
49
+ margin: 0;
50
+ padding: 0;
51
+ line-height: 1.65em;
52
+ }
53
+ pre > span {
54
+ position: relative;
55
+ z-index: 1;
56
+ color: #333;
57
+ }
58
+ .covered {
59
+ color: #AEE6B0;
60
+ }
61
+ .not-covered {
62
+ color: #F79B95;
63
+ }
64
+ .covered, .not-covered {
65
+ background-color: currentColor;
66
+ text-shadow: 0 0 0.25em #FFF;
67
+ font-weight: bold;
68
+ padding: 0.25em 0;
69
+ }
70
+ .ignored {
71
+ color: #888;
72
+ }
73
+ </style>
74
+ </head>
75
+ <body>
76
+ <div id="lines">
77
+ <pre>%{lines}</pre>
78
+ </div>
79
+ <div id="code">%{style_sheet}</div>
80
+ </body>
81
+ </html>
data/lib/marmara.rb ADDED
@@ -0,0 +1,512 @@
1
+ require 'open-uri'
2
+ require 'cgi'
3
+ require 'cgi'
4
+ require 'css_parser'
5
+
6
+ module Marmara
7
+
8
+ PSEUDO_CLASSES = /^((first|last|nth|nth\-last)\-(child|of\-type)|not|empty)/
9
+
10
+ class << self
11
+
12
+ def output_directory
13
+ @output_directory || 'log/css'
14
+ end
15
+
16
+ def output_directory=(dir)
17
+ @output_directory = dir
18
+ end
19
+
20
+ def start_recording
21
+ FileUtils.rm_rf(output_directory)
22
+ ENV['_marmara_record'] = '1'
23
+ end
24
+
25
+ def stop_recording
26
+ ENV['_marmara_record'] = nil
27
+ log "\nCompiling CSS coverage report..."
28
+ FileUtils.mkdir_p(output_directory)
29
+ analyze
30
+ end
31
+
32
+ def recording?
33
+ return ENV['_marmara_record'] == '1'
34
+ end
35
+
36
+ def record(driver)
37
+ sheets = []
38
+ @last_html ||= nil
39
+ html = driver.html
40
+
41
+ # don't do anything if the page hasn't changed
42
+ return if @last_html == html
43
+
44
+ # cache the page so we can check again next time
45
+ @last_html = html
46
+
47
+ # look for all the stylesheets
48
+ driver.all('link[rel="stylesheet"]', visible: false).each do |sheet|
49
+ sheets << sheet[:href]
50
+ end
51
+
52
+ @style_sheets ||= {}
53
+ @style_sheet_rules ||= {}
54
+
55
+ # now parse each style sheet
56
+ sheets.each do |sheet|
57
+ unless @style_sheets[sheet] && @style_sheet_rules[sheet]
58
+ @style_sheet_rules[sheet] = []
59
+ all_selectors = {}
60
+ all_at_rules = []
61
+
62
+ parser = nil
63
+ begin
64
+ parser = CssParser::Parser.new
65
+ parser.load_uri!(sheet, capture_offsets: true)
66
+ rescue
67
+ log "Error reading #{sheet}"
68
+ end
69
+
70
+ unless parser.nil?
71
+ # go over each rule in the sheet
72
+ parser.each_rule_set do |rule, media_types|
73
+ selectors = []
74
+ rule.each_selector do |sel, dec, spec|
75
+ if sel.length > 0
76
+ # we need to look for @keyframes and @font-face coverage differently
77
+ if sel.first == '@'
78
+ rule_type = sel[1..-1]
79
+ at_rule = {
80
+ rule: rule,
81
+ type: :at_rule,
82
+ at_rule_type: rule_type
83
+ }
84
+ case rule_type
85
+ when 'font-face'
86
+ at_rule[:property] = 'font-family'
87
+ at_rule[:value] = rule.get_value('font-family').gsub(/^\s*"(.*?)"\s*;?\s*$/, '\1')
88
+ when /^(\-\w+\-)?keyframes\s+(.*?)\s*$/
89
+ at_rule[:property] = ["#{$1}animation-name", "#{$1}animation"]
90
+ at_rule[:value] = $2
91
+ at_rule[:valueRegex] = [/(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])})\s*(?:,|;?$)/, /(?:^|\s)(?:#{Regexp.escape(at_rule[:value])})(?:\s|;?$)/]
92
+ end
93
+
94
+ at_rule[:valueRegex] ||= /(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])}|\"#{Regexp.escape(at_rule[:value])}\")\s*(?:,|;?$)/
95
+
96
+ # store all the info that we collected about the rule
97
+ @style_sheet_rules[sheet] << at_rule
98
+ else
99
+ # just a regular selector, collect it
100
+ selectors << {
101
+ original: sel,
102
+ queryable: get_safe_selector(sel)
103
+ }
104
+ all_selectors[get_safe_selector(sel)] ||= false
105
+
106
+ # store all the info that we collected about the rule
107
+ @style_sheet_rules[sheet] << {
108
+ rule: rule,
109
+ type: :rule,
110
+ selectors: selectors,
111
+ used_selectors: [false] * selectors.count
112
+ }
113
+ end
114
+ else
115
+ # store all the info that we collected about the rule
116
+ @style_sheet_rules[sheet] << {
117
+ rule: rule,
118
+ type: :unknown
119
+ }
120
+ end
121
+ end
122
+ end
123
+
124
+ # store info about the stylesheet
125
+ @style_sheets[sheet] = {
126
+ css: download_style_sheet(sheet),
127
+ all_selectors: all_selectors,
128
+ all_at_rules: all_at_rules,
129
+ included_with: Set.new
130
+ }
131
+ end
132
+ @style_sheets[sheet][:included_with] += sheets
133
+ end
134
+
135
+ # gather together only the selectors that haven't been spotted yet
136
+ selectors_to_find = @style_sheets[sheet][:all_selectors].select{|k,v|!v}.keys
137
+
138
+ # don't do anything unless we have to
139
+ if selectors_to_find.length > 0
140
+ # and search for them in this document
141
+ found_selectors = evaluate_script("(function(selectors) {
142
+ var results = {};
143
+ for (var i = 0; i < selectors.length; i++) {
144
+ results[selectors[i]] = !!document.querySelector(selectors[i]);
145
+ }
146
+ return results;
147
+ })(#{selectors_to_find.to_json})", driver)
148
+
149
+ # now merge the results back in
150
+ found_selectors.each { |k,v| @style_sheets[sheet][:all_selectors][k] ||= v }
151
+
152
+ # and mark each as used if found
153
+ @style_sheet_rules[sheet].each_with_index do |rule, rule_index|
154
+ if rule[:type] == :rule
155
+ rule[:selectors].each_with_index do |sel, sel_index|
156
+ @style_sheet_rules[sheet][rule_index][:used_selectors][sel_index] ||= @style_sheets[sheet][:all_selectors][sel[:queryable]]
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ def get_safe_selector(sel)
165
+ sel.gsub!(/:+(.+)([^\-\w]|$)/) do |match|
166
+ ending = Regexp.last_match[2]
167
+ Regexp.last_match[1] =~ PSEUDO_CLASSES ? match : ending
168
+ end
169
+ sel.length > 0 ? sel : '*'
170
+ end
171
+
172
+ def get_style_sheet_html
173
+ @style_sheet_html ||= File.read(File.join(File.dirname(__FILE__), 'marmara', 'style-sheet.html'))
174
+ end
175
+
176
+ def evaluate_script(script, driver = @last_driver)
177
+ @last_driver = driver
178
+ @last_driver.evaluate_script(script)
179
+ end
180
+
181
+ def analyze
182
+ # start compiling the overall stats
183
+ overall_stats = {
184
+ 'Rules' => { match_count: 0, total: 0 },
185
+ 'Selectors' => { match_count: 0, total: 0 },
186
+ 'Declarations' => { match_count: 0, total: 0 }
187
+ }
188
+
189
+ # go through all of the style sheets found
190
+ #get_latest_results.each do |uri, rules|
191
+ @style_sheet_rules.each do |uri, rules|
192
+ # download the style sheet
193
+ original_sheet = (@style_sheets[uri] || {})[:css]
194
+
195
+ if original_sheet
196
+ # if we can download it calculate the overage
197
+ coverage = get_coverage(uri) #original_sheet, rules)
198
+ # and generate the report
199
+ html = generate_html_report(original_sheet, coverage[:covered_rules])
200
+
201
+ # output stats for this file
202
+ log_stats(get_report_filename(uri), {
203
+ 'Rules' => {
204
+ match_count: coverage[:matched_rules],
205
+ total: coverage[:total_rules]
206
+ },
207
+ 'Selectors' => {
208
+ match_count: coverage[:matched_selectors],
209
+ total: coverage[:total_selectors]
210
+ },
211
+ 'Declarations' => {
212
+ match_count: coverage[:matched_declarations],
213
+ total: coverage[:total_declarations]
214
+ }
215
+ })
216
+
217
+ # add to the overall stats
218
+ overall_stats['Rules'][:match_count] += coverage[:matched_rules]
219
+ overall_stats['Rules'][:total] += coverage[:total_rules]
220
+ overall_stats['Selectors'][:match_count] += coverage[:matched_selectors]
221
+ overall_stats['Selectors'][:total] += coverage[:total_selectors]
222
+ overall_stats['Declarations'][:match_count] += coverage[:matched_declarations]
223
+ overall_stats['Declarations'][:total] += coverage[:total_declarations]
224
+
225
+ # save the report
226
+ save_report(uri, html)
227
+ end
228
+ end
229
+
230
+ log_stats('Overall', overall_stats)
231
+ log "\n"
232
+ end
233
+
234
+ def download_style_sheet(uri)
235
+ open_attempts = 0
236
+ begin
237
+ open_attempts += 1
238
+ uri = Addressable::URI.parse(uri.to_s)
239
+
240
+ # remote file
241
+ if uri.scheme == 'https'
242
+ uri.port = 443 unless uri.port
243
+ http = Net::HTTP.new(uri.host, uri.port)
244
+ http.use_ssl = true
245
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
246
+ else
247
+ http = Net::HTTP.new(uri.host, uri.port)
248
+ end
249
+
250
+ res = http.get(uri.request_uri, {'Accept-Encoding' => 'gzip'})
251
+ src = res.body.force_encoding("UTF-8")
252
+
253
+ case res['content-encoding']
254
+ when 'gzip'
255
+ io = Zlib::GzipReader.new(StringIO.new(res.body))
256
+ src = io.read
257
+ when 'deflate'
258
+ io = Zlib::Inflate.new
259
+ src = io.inflate(res.body)
260
+ end
261
+
262
+ if String.method_defined?(:encode)
263
+ src.encode!('UTF-8', 'utf-8')
264
+ else
265
+ ic = Iconv.new('UTF-8//IGNORE', 'utf-8')
266
+ src = ic.iconv(src)
267
+ end
268
+
269
+ return src
270
+ rescue Exception => e
271
+ sleep(1)
272
+ retry if open_attempts < 4
273
+ log "\tFailed to open #{uri}"
274
+ log e.to_s
275
+ end
276
+ return nil
277
+ end
278
+
279
+ def save_report(uri, html)
280
+ File.open(get_report_path(uri), 'wb:UTF-8') { |f| f.write(html) }
281
+ end
282
+
283
+ def get_report_path(uri)
284
+ File.join(output_directory, get_report_filename(uri) + '.html')
285
+ end
286
+
287
+ def get_report_filename(uri)
288
+ File.basename(uri)
289
+ end
290
+
291
+ def is_property_covered(sheets, property, valueRegex)
292
+ # iterate over each sheet
293
+ sheets.each do |uri|
294
+ # each rule in each sheet
295
+ @style_sheet_rules[uri].each do |rule|
296
+ # check to see if this property and value matches
297
+ if rule[:type] == :rule
298
+ # if at least one selector was covered we can return true now
299
+ valueRegexs = [*valueRegex]
300
+ [*property].each_with_index do |prop, i|
301
+ if rule[:rule].get_value(prop) =~ valueRegexs[i] && rule[:used_selectors].reduce(&:|)
302
+ return true
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ # the rule wasn't covered
310
+ return false
311
+ end
312
+
313
+ def get_coverage(uri)
314
+ total_selectors = 0
315
+ covered_selectors = 0
316
+
317
+ total_rules = @style_sheet_rules[uri].count
318
+ covered_rules = 0
319
+
320
+ total_declarations = 0
321
+ covered_declarations = 0
322
+
323
+ sheet_covered_rules = []
324
+ @style_sheet_rules[uri].each do |rule|
325
+ coverage = {
326
+ offset: [
327
+ rule[:rule].offset.first,
328
+ rule[:rule].offset.last
329
+ ],
330
+ }
331
+
332
+ if rule[:type] == :at_rule
333
+ covered = is_property_covered(@style_sheets[uri][:included_with], rule[:property], rule[:valueRegex])
334
+
335
+ total_selectors += 1
336
+ total_rules += 1
337
+ total_declarations += 1
338
+
339
+ if covered
340
+ covered_selectors += 1
341
+ covered_rules += 1
342
+ covered_declarations += 1
343
+ coverage[:state] = :covered
344
+ else
345
+ coverage[:state] = :not_covered
346
+ end
347
+ elsif rule[:type] == :rule
348
+ some_covered = rule[:used_selectors].reduce(&:|)
349
+ total_selectors += rule[:used_selectors].count
350
+
351
+ if some_covered
352
+ covered_rules += 1
353
+
354
+ rule[:rule].each_declaration do
355
+ total_declarations += 1
356
+ covered_declarations += 1
357
+ end
358
+
359
+ coverage[:state] = :covered
360
+ if rule[:used_selectors].reduce(&:&)
361
+ covered_selectors += rule[:used_selectors].count
362
+ else
363
+ original_selectors, = @style_sheets[uri][:css].byteslice(rule[:rule].offset).split(/\s*\{/, 2)
364
+ selector_i = 0
365
+
366
+ original_selectors.scan(/(?<=^|,)\s*(.*?)\s*(?=,|$)/m) do |match|
367
+ is_covered = rule[:used_selectors][selector_i] ? :covered : :not_covered
368
+ covered_selectors += 1 if is_covered
369
+ sheet_covered_rules << {
370
+ offset: [
371
+ coverage[:offset][0] + Regexp.last_match.offset(0).first,
372
+ coverage[:offset][0] + Regexp.last_match.offset(0).last
373
+ ],
374
+ state: is_covered
375
+ }
376
+ selector_i += 1
377
+ end
378
+ coverage[:offset][0] += original_selectors.length + 1
379
+ end
380
+ else
381
+ rule[:rule].each_declaration do
382
+ total_declarations += 1
383
+ end
384
+
385
+ coverage[:state] = :not_covered
386
+ end
387
+ end
388
+ sheet_covered_rules << coverage
389
+ end
390
+
391
+ {
392
+ covered_rules: organize_rules(sheet_covered_rules),
393
+ total_rules: total_rules,
394
+ matched_rules: covered_rules,
395
+ total_selectors: total_selectors,
396
+ matched_selectors: covered_selectors,
397
+ total_declarations: total_declarations,
398
+ matched_declarations: covered_declarations,
399
+ }
400
+ end
401
+
402
+ def organize_rules(rules)
403
+ # first sort the rules by the starting index
404
+ rules.sort_by! { |r| r[:offset].first }
405
+
406
+ # then remove unnecessary regions
407
+ i = 0
408
+ rules_removed = false
409
+ while i < rules.length - 1
410
+ # look for empty regions
411
+ if rules[i][:offset][1] <= rules[i][:offset][0]
412
+ # so that we don't lose our place, set the value to nil, then we'll strip the array of nils
413
+ rules[i] = nil
414
+ rules_removed = true
415
+ # look for regions that should be connected
416
+ elsif (next_rule = rules[i + 1]) && rules[i][:offset][1] == next_rule[:offset][0] && rules[i][:state] == next_rule[:state]
417
+ # back up the next rule to start where ours does
418
+ rules[i + 1][:offset][0] = rules[i][:offset][0]
419
+ # and get rid of ourselves
420
+ rules[i] = nil
421
+ rules_removed = true
422
+ end
423
+ i += 1
424
+ end
425
+
426
+ # strip the array of nil values we may have set in the previous step
427
+ rules.compact! if rules_removed
428
+
429
+ # look for overlapping rules
430
+ i = 0
431
+ while i < rules.length
432
+ next_rule = rules[i + 1]
433
+ if next_rule && rules[i][:offset][1] > next_rule[:offset][0]
434
+ # we found an overlapping rule
435
+ # slice up this rule and add the remaining to the end of the array
436
+ rules << {
437
+ offset: [next_rule[:offset][1], rules[i][:offset][1]],
438
+ state: rules[i][:state]
439
+ }
440
+ # and shorten the length of this rule
441
+ rules[i][:offset][1] = next_rule[:offset][0]
442
+
443
+ # start again
444
+ return organize_rules(rules)
445
+ end
446
+ i += 1
447
+ end
448
+
449
+ # we're done!
450
+ return rules
451
+ end
452
+
453
+ def generate_html_report(original_sheet, coverage)
454
+ sheet_html = ''
455
+ last_index = 0
456
+
457
+ # collect the sheet html
458
+ coverage.each do |rule|
459
+ sheet_html += wrap_code(original_sheet.byteslice(last_index...rule[:offset][0]), :ignored)
460
+ sheet_html += wrap_code(original_sheet.byteslice(rule[:offset][0]..rule[:offset][1]), rule[:state])
461
+ last_index = rule[:offset][1] + 1
462
+ end
463
+
464
+ sheet_html += wrap_code(original_sheet[last_index..original_sheet.length], :ignored)
465
+ sheet_html.gsub!(/\n/, '<br>')
466
+ lines = (1..original_sheet.lines.count).to_a.map do |line|
467
+ "<a href=\"#L#{line}\" id=\"L#{line}\">#{line}</a>"
468
+ end
469
+ get_style_sheet_html.gsub('%{lines}', lines.join('')).gsub('%{style_sheet}', sheet_html)
470
+ end
471
+
472
+ def wrap_code(str, state)
473
+ return '' unless str && str.length > 0
474
+
475
+ @state_attr ||= {
476
+ covered: 'class="covered"',
477
+ ignored: 'class="ignored"',
478
+ not_covered: 'class="not-covered"'
479
+ }
480
+ "<pre #{@state_attr[state]}><span>#{CGI.escapeHTML(str)}</span></pre>"
481
+ end
482
+
483
+ def rules_equal?(rule_a, rule_b)
484
+ # sometimes the normalizer isn't very predictable, reset some equivalent rules ere
485
+ @rule_replacements ||= {
486
+ '(\soutline:)\s*(?:0px|0|rgb\(0,\s*0,\s*0\));' => '\1 0;'
487
+ }
488
+
489
+ # make the necessary replacements
490
+ @rule_replacements.each do |regex, replacement|
491
+ rule_a.gsub!(Regexp.new(regex), replacement)
492
+ rule_b.gsub!(Regexp.new(regex), replacement)
493
+ end
494
+
495
+ # and test for equivalence
496
+ return rule_a == rule_b
497
+ end
498
+
499
+ def log_stats(title, report)
500
+ log "\n #{title}:"
501
+
502
+ report.each do |header, data|
503
+ percent = ((data[:match_count] * 100.0) / data[:total]).round(2)
504
+ log " #{header}: #{data[:match_count]}/#{data[:total]} (#{percent}%)"
505
+ end
506
+ end
507
+
508
+ def log(str)
509
+ puts str
510
+ end
511
+ end
512
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: marmara
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Godwin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: css_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.5.0.pre
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.5.0.pre
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Generates a CSS coverage report
84
+ email:
85
+ - goodgodwin@hotmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/marmara.rb
94
+ - lib/marmara/style-sheet.html
95
+ homepage: http://bikecollectives.org
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.6.10
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Analyses your css for code coverage
119
+ test_files: []