critical-path-css-rails 0.4.0 → 1.0.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 +4 -4
- data/.codeclimate.yml +0 -2
- data/.gitignore +3 -0
- data/Gemfile +16 -3
- data/README.md +5 -5
- data/critical-path-css-rails.gemspec +4 -2
- data/ext/npm/extconf.rb +3 -0
- data/ext/npm/install.rb +4 -0
- data/lib/critical-path-css-rails.rb +15 -9
- data/lib/critical_path_css/configuration.rb +8 -18
- data/lib/critical_path_css/css_fetcher.rb +50 -15
- data/lib/critical_path_css/rails/config_loader.rb +24 -0
- data/lib/critical_path_css/rails/version.rb +1 -2
- data/lib/fetch-css.js +14 -0
- data/lib/npm_commands.rb +56 -0
- data/package-lock.json +782 -0
- data/package.json +13 -0
- data/spec/css_fetcher_spec.rb +25 -0
- data/spec/fixtures/static/test.css +3 -0
- data/spec/fixtures/static/test.html +7 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/static_file_server.rb +45 -0
- metadata +21 -9
- data/lib/penthouse/penthouse.js +0 -601
data/package.json
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
{
|
2
|
+
"name": "critical-path-css-rails",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "NPM dependencies of critical-path-css-rails",
|
5
|
+
"private": true,
|
6
|
+
"directories": {
|
7
|
+
"lib": "lib"
|
8
|
+
},
|
9
|
+
"dependencies": {
|
10
|
+
"penthouse": "=0.11.5"
|
11
|
+
},
|
12
|
+
"license": "MIT"
|
13
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'CssFetcher' do
|
6
|
+
before :all do
|
7
|
+
StaticFileServer.start
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
StaticFileServer.stop
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'fetches css' do
|
15
|
+
config = CriticalPathCss::Configuration.new(
|
16
|
+
'base_url' => StaticFileServer.url,
|
17
|
+
'css_path' => 'spec/fixtures/static/test.css',
|
18
|
+
'routes' => ['/test.html']
|
19
|
+
)
|
20
|
+
fetcher = CriticalPathCss::CssFetcher.new(config)
|
21
|
+
expect(fetcher.fetch).to(
|
22
|
+
eq('/test.html' => "p {\n color: red;\n}\n")
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'critical-path-css-rails'
|
5
|
+
|
6
|
+
require 'support/static_file_server'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
# Enable flags like --only-failures and --next-failure
|
10
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
11
|
+
|
12
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
13
|
+
config.disable_monkey_patching!
|
14
|
+
|
15
|
+
config.expect_with :rspec do |c|
|
16
|
+
c.syntax = :expect
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module StaticFileServer
|
4
|
+
class << self
|
5
|
+
def start # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
6
|
+
@port = get_free_port
|
7
|
+
rd, wt = IO.pipe
|
8
|
+
@pid = fork do
|
9
|
+
require 'webrick'
|
10
|
+
rd.close
|
11
|
+
server = WEBrick::HTTPServer.new(
|
12
|
+
DocumentRoot: File.expand_path('spec/fixtures/static'),
|
13
|
+
Port: @port,
|
14
|
+
BindAddress: '127.0.0.1',
|
15
|
+
StartCallback: lambda do
|
16
|
+
# write "1", signal a server start message
|
17
|
+
wt.write(1)
|
18
|
+
wt.close
|
19
|
+
end
|
20
|
+
)
|
21
|
+
trap('INT') { server.shutdown }
|
22
|
+
server.start
|
23
|
+
end
|
24
|
+
wt.close
|
25
|
+
# read a byte for the server start signal
|
26
|
+
rd.read(1)
|
27
|
+
rd.close
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
Process.kill('INT', @pid)
|
32
|
+
end
|
33
|
+
|
34
|
+
def url
|
35
|
+
"http://localhost:#{@port}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_free_port
|
39
|
+
server = TCPServer.new('127.0.0.1', 0)
|
40
|
+
port = server.addr[1]
|
41
|
+
server.close
|
42
|
+
port
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
CHANGED
@@ -1,33 +1,34 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: critical-path-css-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Misshore
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
19
|
+
version: '3.6'
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '3.6'
|
27
27
|
description: Only load the CSS you need for the initial viewport in Rails!
|
28
28
|
email: mmisshore@gmail.com
|
29
29
|
executables: []
|
30
|
-
extensions:
|
30
|
+
extensions:
|
31
|
+
- ext/npm/extconf.rb
|
31
32
|
extra_rdoc_files: []
|
32
33
|
files:
|
33
34
|
- ".codeclimate.yml"
|
@@ -39,15 +40,26 @@ files:
|
|
39
40
|
- LICENSE
|
40
41
|
- README.md
|
41
42
|
- critical-path-css-rails.gemspec
|
43
|
+
- ext/npm/extconf.rb
|
44
|
+
- ext/npm/install.rb
|
42
45
|
- lib/config/critical_path_css.yml
|
43
46
|
- lib/critical-path-css-rails.rb
|
44
47
|
- lib/critical_path_css/configuration.rb
|
45
48
|
- lib/critical_path_css/css_fetcher.rb
|
49
|
+
- lib/critical_path_css/rails/config_loader.rb
|
46
50
|
- lib/critical_path_css/rails/engine.rb
|
47
51
|
- lib/critical_path_css/rails/version.rb
|
52
|
+
- lib/fetch-css.js
|
48
53
|
- lib/generators/critical_path_css/install_generator.rb
|
49
|
-
- lib/
|
54
|
+
- lib/npm_commands.rb
|
50
55
|
- lib/tasks/critical_path_css.rake
|
56
|
+
- package-lock.json
|
57
|
+
- package.json
|
58
|
+
- spec/css_fetcher_spec.rb
|
59
|
+
- spec/fixtures/static/test.css
|
60
|
+
- spec/fixtures/static/test.html
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- spec/support/static_file_server.rb
|
51
63
|
homepage:
|
52
64
|
licenses:
|
53
65
|
- MIT
|
@@ -68,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
80
|
version: '0'
|
69
81
|
requirements: []
|
70
82
|
rubyforge_project:
|
71
|
-
rubygems_version: 2.
|
83
|
+
rubygems_version: 2.6.12
|
72
84
|
signing_key:
|
73
85
|
specification_version: 4
|
74
86
|
summary: Critical Path CSS for Rails!
|
data/lib/penthouse/penthouse.js
DELETED
@@ -1,601 +0,0 @@
|
|
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
|
-
})();
|