3 /* eslint-env phantomjs */
5 /* globals document: true */
7 var path = require('path'),
8 phridge = require('phridge'),
9 promise = require('bluebird'),
10 utility = require('./utility'),
12 _ = require('lodash');
17 * Create the PhantomJS instances, or use the given one.
18 * @param {Object} instance The instance to use, if there is one
21 function init(instance) {
27 // Convert to bluebird promise
28 return new promise(function (resolve) {
29 resolve(phridge.spawn({
30 ignoreSslErrors: 'yes',
33 }).then(function (ph) {
34 /* Phridge outputs everything to stdout by default */
35 ph.childProcess.cleanStdout.unpipe();
36 ph.childProcess.cleanStdout.pipe(process.stderr);
38 }).disposer(phridge.disposeAll);
41 function cleanupAll() {
46 * This function is called whenever a resource is requested by PhantomJS.
47 * If we are loading either raw HTML or a local page, PhantomJS needs to be able to find the
48 * resource with an absolute path.
49 * There are two possible cases:
50 * - 'file://': This might be either a protocol-less URL or a relative path. Since we
51 * can't handle both, we choose to handle the former.
52 * - 'file:///': This is an absolute path. If options.htmlroot is specified, we have a chance to
53 * redirect the request to the correct location.
55 function ResourceHandler(htmlroot, isWindows, resolve) {
56 var ignoredExtensions = ['\\.css', '\\.png', '\\.gif', '\\.jpg', '\\.jpeg', ''],
57 ignoredEndpoints = ['fonts\\.googleapis'];
59 var ignoreRequests = new RegExp(ignoredExtensions.join('$|') + ignoredEndpoints.join('|'));
61 this.onResourceRequested = function (requestData, networkRequest) {
62 var originalUrl = requestData.url,
63 url = originalUrl.split('?')[0].split('#')[0];
65 if (url.substr(-3) === '.js' && url.substr(0, 7) === 'file://') {
66 /* Try and match protocol-less URLs and absolute ones.
67 * Relative URLs will still not load.
69 if (url.substr(5, 3) === '///') {
71 * Retry loading the resource appending the htmlroot option
74 /* Do not strip leading '/' */
75 url = originalUrl.substr(0, 8) + htmlroot + originalUrl.substr(7);
77 url = originalUrl.substr(0, 7) + htmlroot + originalUrl.substr(7);
80 /* Protocol-less URL */
81 url = 'http://' + originalUrl.substr(7);
83 networkRequest.changeUrl(url);
84 } else if (ignoreRequests.test(url)) {
85 networkRequest.abort();
92 * Helper for fromRaw, fromLocal, fromRemote;
93 * return the phantom page after the timeout
95 * @param {phantom} page Page created by phantom
96 * @param {Object} options
99 function resolveWithPage(page, options) {
101 return new promise(function (resolve) {
102 setTimeout(function () {
103 return resolve(page);
110 * Load a page given an HTML string.
111 * @param {String} html
112 * @param {Object} options
115 function fromRaw(html, options) {
116 var page = phantom.createPage(),
117 htmlroot = path.join(process.cwd(), options.htmlroot || '');
119 return promise.resolve(page.run(htmlroot, utility.isWindows(), ResourceHandler).then(function () {
120 return page.run(html, function (raw) {
121 this.setContent(raw, 'local');
123 }).then(resolveWithPage(page, options)));
127 * Open a page given a filename.
128 * @param {String} filename
129 * @param {Object} options
132 function fromLocal(filename, options) {
133 return promise.promisify(fs.readFile)(filename, 'utf-8').then(function (html) {
134 return fromRaw(html, options);
139 * Open a page given a URL.
140 * @param {String} url
141 * @param {Object} options
144 function fromRemote(url, options) {
145 /* If the protocol is unspecified, default to HTTP */
146 if (!/^http/.test(url)) {
150 return promise.resolve(phantom.openPage(url).then(function (page) {
151 return resolveWithPage(page, options)();
156 * Extract stylesheets' hrefs from dom
157 * @param {Object} page A PhantomJS page
158 * @param {Object} options Options, as passed to UnCSS
161 function getStylesheets(page, options) {
162 if (_.isArray(options.media) === false) {
163 options.media = [options.media];
165 var media = _.union(['', 'all', 'screen'], options.media);
166 return page.run(function () {
167 return this.evaluate(function () {
168 return Array.prototype.map.call(document.querySelectorAll('link[rel="stylesheet"]'), function (link) {
175 }).then(function (stylesheets) {
177 .toArray(stylesheets)
178 /* Match only specified media attributes, plus defaults */
179 .filter(function (sheet) {
180 return media.indexOf(sheet.media) !== -1;
182 .map(function (sheet) {
190 * Filter unused selectors.
191 * @param {Object} page A PhantomJS page
192 * @param {Array} sels List of selectors to be filtered
195 function findAll(page, sels) {
196 return page.run(sels, function (args) {
197 return this.evaluate(function (selectors) {
198 // Unwrap noscript elements
199 Array.prototype.forEach.call(document.getElementsByTagName('noscript'), function (ns) {
200 var wrapper = document.createElement('div');
201 wrapper.innerHTML = ns.innerText;
202 // Insert each child of the <noscript> as its sibling
203 Array.prototype.forEach.call(wrapper.children, function (child) {
204 ns.parentNode.insertBefore(child, ns);
208 selectors = selectors.filter(function (selector) {
210 if (document.querySelector(selector)) {
222 }).then(function (res) {
226 return res.selectors;
232 cleanupAll: cleanupAll,
233 fromLocal: fromLocal,
235 fromRemote: fromRemote,
237 getStylesheets: getStylesheets