critical-path-css-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c7c88b5a77a5230fc8b1f7d39cedb1fb818bc786
4
+ data.tar.gz: f7ba36dd7fe8c3392745205be27647b2a93bda70
5
+ SHA512:
6
+ metadata.gz: 3101d6628e5433b0d236a4541b68f1dd9d6b46f7e22b5d7b638216c6f07bf4697c6ebd7b4105bd01e2d19296f40bd389ed59c39d77dac911455422cea824fc3d
7
+ data.tar.gz: 6dc6cc6732ed93a6932dc9288ac0acb81d82cf0806343e60ffdee71c5b729243113ce699a6356896409d99c85193f70c062197eda9ec71610148826c8ccfe557
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .DS_Store
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - '*.gemspec'
6
+ - 'spec/*_helper.rb'
7
+ - 'Gemfile'
8
+ - 'Rakefile'
9
+ - 'Vagrantfile'
10
+
11
+ Documentation:
12
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,12 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2015-09-19 12:37:55 -0500 using RuboCop version 0.34.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ # Configuration parameters: AllowURI, URISchemes.
11
+ Metrics/LineLength:
12
+ Max: 91
data/BACKLOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Backlog
2
+
3
+ ## Tests
4
+ - Add a testing suite (preferably rspec)
5
+
6
+ ## Features
7
+ - Allow the user to pass arguments to Penthouse.js, i.e. Viewport size, etc. For a list of the configurable options, please see [Penthouse](https://github.com/pocketjoso/penthouse)
8
+ - Improve installation process, if necessary
9
+ - Improve manual processes, if possible (i.e having to run the rake task to generate the the critical CSS)
10
+ - Improve implementation. Is their a better solution then using Rails.cache?
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gem 'rubocop', require: false
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) [2015] [Mudbug Media, Michael Misshore]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # critical-path-css-rails
2
+
3
+ Only load the CSS you need for the initial viewport in Rails!
4
+
5
+ This gem give you the ability to load only the CSS you *need* on an initial page view. This gives you blazin' fast rending as there's no initial network call to grab your application's CSS.
6
+
7
+ This gem assumes that you'll load the rest of the CSS asyncronously. At the moment, the suggested way is to use the [loadcss-rails](https://github.com/michael-misshore/loadcss-rails) gem.
8
+
9
+ This gem uses [PhantomJS](https://github.com/colszowka/phantomjs-gem) and [Penthouse](https://github.com/pocketjoso/penthouse) to generate the critical CSS.
10
+
11
+
12
+ ## Installation
13
+
14
+ Add `critical-path-css-rails` to your Gemfile:
15
+
16
+ ```
17
+ gem 'critical-path-css-rails', '~> 0.1.0'
18
+ ```
19
+
20
+ Download and install by running:
21
+
22
+ ```
23
+ bundle install
24
+ ```
25
+
26
+ Create the rake task that will generate your critical CSS
27
+
28
+ ```
29
+ rails generator critical_path_css:install
30
+ ```
31
+
32
+ This adds the following file:
33
+
34
+ * `lib/tasks/critical_path_css.rake`
35
+
36
+
37
+ ## Usage
38
+
39
+ First, you'll need to configue a few variables in the rake task: `lib/tasks/critical_path_css.rake`
40
+
41
+ * `@base_url`: Change the url's here to match your Production and Development base URL, respectively.
42
+ * `@routes`: List the routes that you would like to generate the critical CSS for. (i.e. /resources, /resources/show/1, etc.)
43
+ * `@main_css_path`: Inside of the generate task, you'll need to define the path to the application's main CSS. The gem assumes your CSS lives in `RAILS_ROOT/public`. If your main CSS file is in `RAILS_ROOT/public/assets/main.css`, you would set the variable to `/assets/main.css`.
44
+
45
+
46
+ Before generating the CSS, ensure that your application is running (viewable from a browser) and the main CSS file exists. Then in a separate tab, run the rake task to generate the critical CSS.
47
+
48
+ If your main CSS file does not already exist, and you are using the Asset Pipeline, generate the main CSS file.
49
+ ```
50
+ rake assets:precompile
51
+ ```
52
+ Generate the critical path CSS:
53
+ ```
54
+ rake critical_path_css:generate
55
+ ```
56
+
57
+
58
+ To load the generated critical CSS into your layout, in the head tag, insert:
59
+
60
+ ```html
61
+ <style>
62
+ <%= CriticalPathCss.fetch(request.path) %>
63
+ </style>
64
+ ```
65
+
66
+ A simple example using loadcss-rails looks like:
67
+
68
+ ```html
69
+ <style>
70
+ <%= CriticalPathCss.fetch(request.path) %>
71
+ </style>
72
+ <script>
73
+ loadCSS("<%= stylesheet_path('application') %>");
74
+ </script>
75
+ <noscript>
76
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
77
+ </noscript>
78
+ ```
79
+
80
+
81
+ ## Versions
82
+
83
+ The critical-path-css-rails gem follows these version guidelines:
84
+
85
+ ```
86
+ patch version bump = updates to critical-path-css-rails and patch-level updates to Penthouse and PhantomJS
87
+ minor version bump = minor-level updates to critical-path-css-rails, Penthouse, and PhantomJS
88
+ major version bump = major-level updates to critical-path-css-rails, Penthouse, PhantomJS, and updates to Rails which may be backwards-incompatible
89
+ ```
90
+
91
+ ## Contributing
92
+
93
+ Feel free to open an issue ticket if you find something that could be improved. A couple notes:
94
+
95
+ * If the Penthouse.js script is outdated (i.e. maybe a new version of Penthouse.js was released yesterday), feel free to open an issue and prod us to get that thing updated. However, for security reasons, we won't be accepting pull requests with updated Penthouse.js script.
96
+
97
+ Copyright Mudbug Media and Michael Misshore, released under the MIT License.
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../lib/critical_path_css/rails/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'critical-path-css-rails'
5
+ s.version = CriticalPathCSS::Rails::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ['Michael Misshore']
8
+ s.email = 'mmisshore@gmail.com'
9
+ s.summary = 'Critical Path CSS for Rails!'
10
+ s.description = 'Only load the CSS you need for the initial viewport in Rails!'
11
+ s.license = 'MIT'
12
+
13
+ s.add_runtime_dependency 'phantomjs', ['~> 1.9']
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.require_path = 'lib'
18
+ end
@@ -0,0 +1,6 @@
1
+ module CriticalPathCss
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module CriticalPathCSS
2
+ module Rails
3
+ VERSION = '0.1.0'
4
+ PENTHOUSE_VERSION = '0.3.4'
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ module CriticalPathCss
2
+ require 'phantomjs'
3
+
4
+ CACHE_NAMESPACE = 'critical-path-css'
5
+ PENTHOUSE_PATH = "#{File.dirname(__FILE__)}/penthouse/penthouse.js"
6
+
7
+ def self.generate(main_css_path, base_url, routes)
8
+ full_main_css_path = "#{Rails.root}/public#{main_css_path}"
9
+
10
+ routes.each do |route|
11
+ css = Phantomjs.run(PENTHOUSE_PATH, base_url + route, full_main_css_path)
12
+ Rails.cache.write(route, css, namespace: CACHE_NAMESPACE)
13
+ end
14
+ end
15
+
16
+ def self.fetch(route)
17
+ Rails.cache.read(route, namespace: CACHE_NAMESPACE) || ''
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module CriticalPathCss
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('..', __FILE__)
6
+
7
+ # Copy the needed rake task for generating critical CSS
8
+ def copy_rake_task
9
+ task_filename = 'critical_path_css.rake'
10
+ copy_file "../../tasks/#{task_filename}", "lib/tasks/#{task_filename}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,601 @@
1
+ /*
2
+ Penthouse CSS Critical Path Generator
3
+ https://github.com/pocketjoso/penthouse
4
+ Author: Jonas Ohlsson
5
+ License: MIT
6
+ Version: 0.3.4
7
+
8
+ USAGE:
9
+ phantomjs penthouse.js [options] <URL to page> <CSS file>
10
+ Options:
11
+ --width <width> The viewport width in pixels. Defaults to 1300
12
+ --height <height> The viewport height in pixels. Defaults to 900
13
+
14
+ to run on HTTPS sites two flags must be passed in, directly after phantomjs in the call:
15
+ --ignore-ssl-errors=true --ssl-protocol=tlsv1
16
+
17
+ DEPENDENCIES
18
+ + "phantomjs" : "~1.9.7"
19
+
20
+ */
21
+
22
+
23
+ (function() { "use strict";
24
+ /*
25
+ * parser for the script - can be used both for the standalone node binary and the phantomjs script
26
+ */
27
+
28
+ /*jshint unused:false*/
29
+
30
+ var usageString = '[--width <width>] [--height <height>] <url> <main.css>';
31
+
32
+ function buildError(msg, problemToken, args) {
33
+ var error = new Error(msg + problemToken);
34
+ error.token = problemToken;
35
+ error.args = args;
36
+ throw error;
37
+ }
38
+
39
+ // Parses the arguments passed in
40
+ // @returns { width, height, url, css }
41
+ // throws an error on wrong options or parsing error
42
+ function parseOptions(argsOriginal) {
43
+ var args = argsOriginal.slice(0),
44
+ validOptions = ['--width', '--height'],
45
+ parsed = {},
46
+ val,
47
+ len = args.length,
48
+ optIndex,
49
+ option;
50
+
51
+ if (len < 2) buildError('Not enough arguments, ', args);
52
+
53
+ while (args.length > 2 && args[0].match(/^(--width|--height)$/)) {
54
+ optIndex = validOptions.indexOf(args[0]);
55
+ if (optIndex === -1) buildError('Logic/Parsing error ', args[0], args);
56
+
57
+ // lose the dashes
58
+ option = validOptions[optIndex].slice(2);
59
+ val = args[1];
60
+
61
+ parsed[option] = parseInt(val, 10);
62
+ if (isNaN(parsed[option])) buildError('Parsing error when parsing ', val, args);
63
+
64
+ // remove the two parsed arguments from the list
65
+ args = args.slice(2);
66
+ }
67
+ parsed.url = args[0];
68
+ parsed.css = args[1];
69
+
70
+ if (!parsed.url) {
71
+ buildError('Missing url/path to html file', '', args);
72
+ }
73
+
74
+ if (!parsed.css) {
75
+ buildError('Missing css file', '', args);
76
+ }
77
+
78
+
79
+ return parsed;
80
+ }
81
+
82
+ if (typeof module !== 'undefined') {
83
+ module.exports = exports = {
84
+ parse: parseOptions,
85
+ usage: usageString
86
+ };
87
+ }
88
+ /*
89
+ module for removing unused fontface rules - can be used both for the standalone node binary and the phantomjs script
90
+ */
91
+ /*jshint unused:false*/
92
+
93
+ function unusedFontfaceRemover (css){
94
+ var toDeleteSections = [];
95
+
96
+ //extract full @font-face rules
97
+ var fontFaceRegex = /(@font-face[ \s\S]*?\{([\s\S]*?)\})/gm,
98
+ ff;
99
+
100
+ while ((ff = fontFaceRegex.exec(css)) !== null) {
101
+
102
+ //grab the font name declared in the @font-face rule
103
+ //(can still be in quotes, f.e. 'Lato Web'
104
+ var t = /font-family[^:]*?:[ ]*([^;]*)/.exec(ff[1]);
105
+ if (typeof t[1] === 'undefined')
106
+ continue; //no font-family in @fontface rule!
107
+
108
+ //rm quotes
109
+ var fontName = t[1].replace(/['"]/gm, '');
110
+
111
+ // does this fontname appear as a font-family or font (shorthand) value?
112
+ var fontNameRegex = new RegExp('([^{}]*?){[^}]*?font(-family)?[^:]*?:[^;]*' + fontName + '[^,;]*[,;]', 'gmi');
113
+
114
+
115
+ var fontFound = false,
116
+ m;
117
+
118
+ while ((m = fontNameRegex.exec(css)) !== null) {
119
+ if (m[1].indexOf('@font-face') === -1) {
120
+ //log('FOUND, keep rule');
121
+ fontFound = true;
122
+ break;
123
+ }
124
+ }
125
+ if (!fontFound) {
126
+ //NOT FOUND, rm!
127
+
128
+ //can't remove rule here as it will screw up ongoing while (exec ...) loop.
129
+ //instead: save indices and delete AFTER for loop
130
+ var closeRuleIndex = css.indexOf('}', ff.index);
131
+ //unshift - add to beginning of array - we need to remove rules in reverse order,
132
+ //otherwise indeces will become incorrect again.
133
+ toDeleteSections.unshift({
134
+ start: ff.index,
135
+ end: closeRuleIndex + 1
136
+ });
137
+ }
138
+ }
139
+ //now delete the @fontface rules we registed as having no matches in the css
140
+ for (var i = 0; i < toDeleteSections.length; i++) {
141
+ var start = toDeleteSections[i].start,
142
+ end = toDeleteSections[i].end;
143
+ css = css.substring(0, start) + css.substring(end);
144
+ }
145
+
146
+ return css;
147
+ };
148
+
149
+
150
+
151
+ if(typeof module !== 'undefined') {
152
+ module.exports = unusedFontfaceRemover;
153
+ }
154
+ /*jshint unused:false*/
155
+
156
+ /* === preFormatCSS ===
157
+ * preformats the css to ensure we won't run into and problems in our parsing
158
+ * removes comments (actually would be anough to remove/replace {} chars.. TODO
159
+ * replaces } char inside content: '' properties.
160
+ */
161
+
162
+ function cssPreformatter (css){
163
+ //remove comments from css (including multi-line coments)
164
+ css = css.replace(/\/\*[\s\S]*?\*\//g, '');
165
+
166
+ //replace Windows \r\n with \n,
167
+ //otherwise final output might get converted into /r/r/n
168
+ css = css.replace(/\r\n/gm, '\n');
169
+
170
+ //we also need to replace eventual close curly bracket characters inside content: '' property declarations, replace them with their ASCI code equivalent
171
+ //\7d (same as: '\' + '}'.charCodeAt(0).toString(16) );
172
+
173
+ var m,
174
+ regexP = /(content\s*:\s*['"][^'"]*)}([^'"]*['"])/gm,
175
+ matchedData = [];
176
+
177
+ //for each content: '' rule that contains at least one end bracket ('}')
178
+ while ((m = regexP.exec(css)) !== null) {
179
+ //we need to replace ALL end brackets in the rule
180
+ //we can't do it in here, because it will mess up ongoing exec, store data and do after
181
+
182
+ //unshift - add to beginning of array - we need to remove rules in reverse order,
183
+ //otherwise indeces will become incorrect.
184
+ matchedData.unshift({
185
+ start: m.index,
186
+ end: m.index + m[0].length,
187
+ replaceStr: m[0].replace(/\}/gm, '\\7d')
188
+ });
189
+ }
190
+
191
+ for (var i = 0; i < matchedData.length; i++) {
192
+ var item = matchedData[0];
193
+ css = css.substring(0, item.start) + item.replaceStr + css.substring(item.end);
194
+ }
195
+
196
+ return css;
197
+ };
198
+
199
+ if(typeof module !== 'undefined') {
200
+ module.exports = cssPreformatter;
201
+ }
202
+ var standaloneMode = true;
203
+ 'use strict';
204
+ var standaloneMode = standaloneMode || false;
205
+
206
+ var page = require('webpage').create(),
207
+ fs = require('fs'),
208
+ system = require('system'),
209
+ DEBUG = false,
210
+ stdout = system.stdout; // for using this as a file
211
+
212
+ var combineArgsString = function(argsArr) {
213
+ return [].join.call(argsArr, ' ') + '\n';
214
+ };
215
+
216
+ // monkey patch for directing errors to stderr
217
+ // https://github.com/ariya/phantomjs/issues/10150#issuecomment-28707859
218
+ var errorlog = function() {
219
+ system.stderr.write(combineArgsString(arguments));
220
+ };
221
+
222
+ var debug = function() {
223
+ if (DEBUG) errorlog('DEBUG: ' + combineArgsString(arguments));
224
+ };
225
+
226
+ // discard stdout from phantom exit;
227
+ var phantomExit = function(code) {
228
+ if (page) {
229
+ page.close();
230
+ }
231
+ setTimeout(function() {
232
+ phantom.exit(code);
233
+ }, 0);
234
+ };
235
+
236
+ //don't confuse analytics more than necessary when visiting websites
237
+ page.settings.userAgent = 'Penthouse Critical Path CSS Generator';
238
+
239
+ /* prevent page JS errors from being output to final CSS */
240
+ page.onError = function(msg, trace) {
241
+ //do nothing
242
+ };
243
+
244
+ page.onResourceError = function(resourceError) {
245
+ page.reason = resourceError.errorString;
246
+ page.reason_url = resourceError.url;
247
+ };
248
+
249
+ var main = function(options) {
250
+ debug('main(): ', JSON.stringify(options));
251
+ //final cleanup
252
+ //remove all empty rules, and remove leading/trailing whitespace
253
+ try {
254
+ var f = fs.open(options.css, 'r');
255
+
256
+ //preformat css
257
+ var cssPreformat;
258
+ if (standaloneMode) {
259
+ cssPreformat = cssPreformatter;
260
+ } else {
261
+ cssPreformat = require('./css-preformatter.js');
262
+ }
263
+ options.css = cssPreformat(f.read());
264
+ } catch (e) {
265
+ errorlog(e);
266
+ phantomExit(1);
267
+ }
268
+
269
+ // start the critical path CSS generation
270
+ getCriticalPathCss(options);
271
+ };
272
+
273
+ function cleanup(css) {
274
+ //remove all animation rules, as keyframes have already been removed
275
+ css = css.replace(/(-webkit-|-moz-|-ms-|-o-)?animation[ ]?:[^;{}]*;/gm, '');
276
+ //remove all empty rules, and remove leading/trailing whitespace
277
+ return css.replace(/[^{}]*\{\s*\}/gm, '').trim();
278
+ }
279
+
280
+ /* Final function
281
+ * Get's called from getCriticalPathCss when CSS extraction from page is done*/
282
+ page.onCallback = function(css) {
283
+ debug('phantom.onCallback');
284
+
285
+ try {
286
+ if (css) {
287
+ // we are done - clean up the final css
288
+ var finalCss = cleanup(css);
289
+
290
+ // remove unused @fontface rules
291
+ var ffRemover;
292
+ if (standaloneMode) {
293
+ ffRemover = unusedFontfaceRemover;
294
+ } else {
295
+ ffRemover = require('./unused-fontface-remover.js');
296
+ }
297
+ finalCss = ffRemover(finalCss);
298
+
299
+ if(finalCss.trim().length === 0){
300
+ errorlog('Note: Generated critical css was empty for URL: ' + options.url);
301
+ }
302
+
303
+ // return the critical css!
304
+ stdout.write(finalCss);
305
+ phantomExit(0);
306
+ } else {
307
+ // No css. This is not an error on our part
308
+ // but still safer to warn the end user, in case they made a mistake
309
+ errorlog('Note: Generated critical css was empty for URL: ' + options.url);
310
+ // for consisteny, still generate output (will be empty)
311
+ stdout.write(css);
312
+ phantomExit(0);
313
+ }
314
+
315
+ } catch (ex) {
316
+ debug('phantom.onCallback -> error', ex);
317
+ errorlog('error: ' + ex);
318
+ phantomExit(1);
319
+ }
320
+ };
321
+
322
+ /*
323
+ * Tests each selector in css file at specified resolution,
324
+ * to see if any such elements appears above the fold on the page
325
+ * modifies CSS - removes selectors that don't appear, and empty rules
326
+ *
327
+ * @param options.url the url as a string
328
+ * @param options.css the css as a string
329
+ * @param options.width the width of viewport
330
+ * @param options.height the height of viewport
331
+ ---------------------------------------------------------*/
332
+ function getCriticalPathCss(options) {
333
+ debug('getCriticalPathCss():', JSON.stringify(options));
334
+
335
+ page.viewportSize = {
336
+ width: options.width,
337
+ height: options.height
338
+ };
339
+
340
+ page.open(options.url, function(status) {
341
+ if (status !== 'success') {
342
+ errorlog('Error opening url \'' + page.reason_url + '\': ' + page.reason);
343
+ phantomExit(1);
344
+ } else {
345
+
346
+ debug('Starting sandboxed evaluation of CSS\n', options.css);
347
+ // sandboxed environments - no outside references
348
+ // arguments and return value must be primitives
349
+ // @see http://phantomjs.org/api/webpage/method/evaluate.html
350
+ page.evaluate(function sandboxed(css) {
351
+ var h = window.innerHeight,
352
+ renderWaitTime = 100, //ms TODO: user specifiable through options object
353
+ finished = false,
354
+ currIndex = 0,
355
+ forceRemoveNestedRule = false;
356
+
357
+ //split CSS so we can value the (selector) rules separately.
358
+ //but first, handle stylesheet initial non nested @-rules.
359
+ //they don't come with any associated rules, and should all be kept,
360
+ //so just keep them in critical css, but don't include them in split
361
+ var splitCSS = css.replace(/@(import|charset|namespace)[^;]*;/g, '');
362
+ var split = splitCSS.split(/[{}]/g);
363
+
364
+ var getNewValidCssSelector = function(i) {
365
+ var newSel = split[i];
366
+ /* HANDLE Nested @-rules */
367
+
368
+ /*Case 1: @-rule with CSS properties inside [REMAIN]
369
+ Can't remove @font-face rules here, don't know if used or not.
370
+ Another check at end for this purpose.
371
+ */
372
+ if (/@(font-face)/gi.test(newSel)) {
373
+ //skip over this rule
374
+ currIndex = css.indexOf('}', currIndex) + 1;
375
+ return getNewValidCssSelector(i + 2);
376
+ }
377
+ /*Case 2: @-rule with CSS properties inside [REMOVE]
378
+ @page
379
+ This case doesn't need any special handling,
380
+ as this "selector" won't match anything on the page,
381
+ and will therefor be removed, together with it's css props
382
+ */
383
+
384
+ /*Case 4: @-rule with full CSS (rules) inside [REMOVE]
385
+ @media print|speech|aural, @keyframes
386
+ Delete this rule and all its contents - doesn't belong in critical path CSS
387
+ */
388
+ else if (/@(media (print|speech|aural)|(([a-z\-])*keyframes))/gi.test(newSel)) {
389
+ //force delete on child css rules
390
+ forceRemoveNestedRule = true;
391
+ return getNewValidCssSelector(i + 1);
392
+ }
393
+
394
+ /*Case 3: @-rule with full CSS (rules) inside [REMAIN]
395
+ This test is executed AFTER Case 4,
396
+ since we here match every remaining @media,
397
+ after @media print has been removed by Case 4 rule)
398
+ - just skip this particular line (i.e. keep), and continue checking the CSS inside as normal
399
+ */
400
+ else if (/@(media|(-moz-)?document|supports)/gi.test(newSel)) {
401
+ return getNewValidCssSelector(i + 1);
402
+ }
403
+ /*
404
+ Resume normal execution after end of @-media rule with inside CSS rules (Case 3)
405
+ Also identify abrupt file end.
406
+ */
407
+ else if (newSel.trim().length === 0) {
408
+ //abrupt file end
409
+ if (i + 1 >= split.length) {
410
+ //end of file
411
+ finished = true;
412
+ return false;
413
+ }
414
+ //end of @-rule (Case 3)
415
+ forceRemoveNestedRule = false;
416
+ return getNewValidCssSelector(i + 1);
417
+ }
418
+ return i;
419
+ };
420
+
421
+ var removeSelector = function(sel, selectorsKept) {
422
+ var selPos = css.indexOf(sel, currIndex);
423
+
424
+ //check what comes next: { or ,
425
+ var nextComma = css.indexOf(',', selPos);
426
+ var nextOpenBracket = css.indexOf('{', selPos);
427
+
428
+ if (selectorsKept > 0 || (nextComma > 0 && nextComma < nextOpenBracket)) {
429
+ //we already kept selectors from this rule, so rule will stay
430
+
431
+ //more selectors in selectorList, cut until (and including) next comma
432
+ if (nextComma > 0 && nextComma < nextOpenBracket) {
433
+ css = css.substring(0, selPos) + css.substring(nextComma + 1);
434
+ }
435
+ //final selector, cut until open bracket. Also remove previous comma, as the (new) last selector should not be followed by a comma.
436
+ else {
437
+ var prevComma = css.lastIndexOf(',', selPos);
438
+ css = css.substring(0, prevComma) + css.substring(nextOpenBracket);
439
+ }
440
+ } else {
441
+ //no part of selector (list) matched elements above fold on page - remove whole rule CSS rule
442
+ var endRuleBracket = css.indexOf('}', nextOpenBracket);
443
+
444
+ css = css.substring(0, selPos) + css.substring(endRuleBracket + 1);
445
+ }
446
+ };
447
+
448
+
449
+ var processCssRules = function() {
450
+ for (var i = 0; i < split.length; i = i + 2) {
451
+ //step over non DOM CSS selectors (@-rules)
452
+ i = getNewValidCssSelector(i);
453
+
454
+ //reach end of CSS
455
+ if (finished) {
456
+ //call final function to exit outside of phantom evaluate scope
457
+ window.callPhantom(css);
458
+ }
459
+
460
+ var fullSel = split[i];
461
+ //fullSel can contain combined selectors
462
+ //,f.e. body, html {}
463
+ //split and check one such selector at the time.
464
+ var selSplit = fullSel.split(',');
465
+ //keep track - if we remove all selectors, we also want to remove the whole rule.
466
+ var selectorsKept = 0;
467
+ var aboveFold;
468
+
469
+ for (var j = 0; j < selSplit.length; j++) {
470
+ var sel = selSplit[j];
471
+
472
+ //some selectors can't be matched on page.
473
+ //In these cases we test a slightly modified selectors instead, temp.
474
+ var temp = sel;
475
+
476
+ if (sel.indexOf(':') > -1) {
477
+ //handle special case selectors, the ones that contain a semi colon (:)
478
+ //many of these selectors can't be matched to anything on page via JS,
479
+ //but that still might affect the above the fold styling
480
+
481
+ //these psuedo selectors depend on an element,
482
+ //so test element instead (would do the same for f.e. :hover, :focus, :active IF we wanted to keep them for critical path css, but we don't)
483
+ temp = temp.replace(/(:?:before|:?:after)*/g, '');
484
+
485
+ //if selector is purely psuedo (f.e. ::-moz-placeholder), just keep as is.
486
+ //we can't match it to anything on page, but it can impact above the fold styles
487
+ if (temp.replace(/:[:]?([a-zA-Z0-9\-\_])*/g, '').trim().length === 0) {
488
+ currIndex = css.indexOf(sel, currIndex) + sel.length;
489
+ selectorsKept++;
490
+ continue;
491
+ }
492
+
493
+ //handle browser specific psuedo selectors bound to elements,
494
+ //Example, button::-moz-focus-inner, input[type=number]::-webkit-inner-spin-button
495
+ //remove browser specific pseudo and test for element
496
+ temp = temp.replace(/:?:-[a-z-]*/g, '');
497
+ }
498
+
499
+ if (!forceRemoveNestedRule) {
500
+ //now we have a selector to test, first grab any matching elements
501
+ var el;
502
+ try {
503
+ el = document.querySelectorAll(temp);
504
+ } catch (e) {
505
+ //not a valid selector, remove it.
506
+ removeSelector(sel, 0);
507
+ continue;
508
+ }
509
+
510
+ //check if selector matched element(s) on page..
511
+ aboveFold = false;
512
+
513
+ for (var k = 0; k < el.length; k++) {
514
+ var testEl = el[k];
515
+ //temporarily force clear none in order to catch elements that clear previous content themselves and who w/o their styles could show up unstyled in above the fold content (if they rely on f.e. 'clear:both;' to clear some main content)
516
+ testEl.style.clear = 'none';
517
+
518
+ //check to see if any matched element is above the fold on current page
519
+ //(in current viewport size)
520
+ if (testEl.getBoundingClientRect().top < h) {
521
+ //then we will save this selector
522
+ aboveFold = true;
523
+ selectorsKept++;
524
+
525
+ //update currIndex so we only search from this point from here on.
526
+ currIndex = css.indexOf(sel, currIndex);
527
+
528
+ //set clear style back to what it was
529
+ testEl.style.clear = '';
530
+ //break, because matching 1 element is enough
531
+ break;
532
+ }
533
+ //set clear style back to what it was
534
+ testEl.style.clear = '';
535
+ }
536
+ } else {
537
+ aboveFold = false;
538
+ } //force removal of selector
539
+
540
+ //if selector didn't match any elements above fold - delete selector from CSS
541
+ if (aboveFold === false) {
542
+ //update currIndex so we only search from this point from here on.
543
+ currIndex = css.indexOf(sel, currIndex);
544
+ //remove seletor (also removes rule, if nnothing left)
545
+ removeSelector(sel, selectorsKept);
546
+ }
547
+ }
548
+ //if rule stayed, move our cursor forward for matching new selectors
549
+ if (selectorsKept > 0) {
550
+ currIndex = css.indexOf('}', currIndex) + 1;
551
+ }
552
+ }
553
+
554
+ //we're done - call final function to exit outside of phantom evaluate scope
555
+ window.callPhantom(css);
556
+ };
557
+
558
+ //give some time (renderWaitTime) for sites like facebook that build their page dynamically,
559
+ //otherwise we can miss some selectors (and therefor rules)
560
+ //--tradeoff here: if site is too slow with dynamic content,
561
+ // it doesn't deserve to be in critical path.
562
+ setTimeout(processCssRules, renderWaitTime);
563
+
564
+ }, options.css);
565
+ }
566
+ });
567
+ }
568
+
569
+ var parser, parse, usage, options;
570
+
571
+ // test to see if we are running as a standalone script
572
+ // or as part of the node module
573
+ if (standaloneMode) {
574
+ parse = parseOptions;
575
+ usage = usageString;
576
+ } else {
577
+ parser = require('../options-parser');
578
+ parse = parser.parse;
579
+ usage = parser.usage;
580
+ }
581
+
582
+ try {
583
+ options = parse(system.args.slice(1));
584
+ } catch (ex) {
585
+
586
+ errorlog('Caught error parsing arguments: ' + ex.message);
587
+
588
+ // the usage string does not make sense to show if running via Node
589
+ if(standaloneMode) {
590
+ errorlog('\nUsage: phantomjs penthouse.js ' + usage);
591
+ }
592
+
593
+ phantomExit(1);
594
+ }
595
+
596
+ // set defaults
597
+ if (!options.width) options.width = 1300;
598
+ if (!options.height) options.height = 900;
599
+
600
+ main(options);
601
+ })();
@@ -0,0 +1,15 @@
1
+ require 'critical_path_css_rails'
2
+
3
+ namespace :critical_path_css do
4
+ @base_url = Rails.env.production? ? 'http://example.com' : 'http://localhost:3000'
5
+ @routes = %w(
6
+ /
7
+ )
8
+
9
+ desc 'Generate critical CSS for the routes defined'
10
+ task generate: :environment do
11
+ @main_css_path = ActionController::Base.helpers.stylesheet_path('application.css').to_s
12
+
13
+ CriticalPathCss.generate(@main_css_path, @base_url, @routes)
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: critical-path-css-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Misshore
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: phantomjs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ description: Only load the CSS you need for the initial viewport in Rails!
28
+ email: mmisshore@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".gitignore"
34
+ - ".rubocop.yml"
35
+ - ".rubocop_todo.yml"
36
+ - BACKLOG.md
37
+ - Gemfile
38
+ - LICENSE
39
+ - README.md
40
+ - critical-path-css-rails.gemspec
41
+ - lib/critical_path_css/rails/engine.rb
42
+ - lib/critical_path_css/rails/version.rb
43
+ - lib/critical_path_css_rails.rb
44
+ - lib/generators/critical_path_css/install_generator.rb
45
+ - lib/penthouse/penthouse.js
46
+ - lib/tasks/critical_path_css.rake
47
+ homepage:
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 2.4.8
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Critical Path CSS for Rails!
71
+ test_files: []