--- /dev/null
+'use strict';
+
+/* eslint-env phantomjs */
+
+/* globals document: true */
+
+var path = require('path'),
+ phridge = require('phridge'),
+ promise = require('bluebird'),
+ utility = require('./utility'),
+ fs = require('fs'),
+ _ = require('lodash');
+
+var phantom;
+
+/**
+ * Create the PhantomJS instances, or use the given one.
+ * @param {Object} instance The instance to use, if there is one
+ * @return {promise}
+ */
+function init(instance) {
+ if (instance) {
+ phantom = instance;
+ return null;
+ }
+
+ // Convert to bluebird promise
+ return new promise(function (resolve) {
+ resolve(phridge.spawn({
+ ignoreSslErrors: 'yes',
+ sslProtocol: 'any'
+ }));
+ }).then(function (ph) {
+ /* Phridge outputs everything to stdout by default */
+ ph.childProcess.cleanStdout.unpipe();
+ ph.childProcess.cleanStdout.pipe(process.stderr);
+ phantom = ph;
+ }).disposer(phridge.disposeAll);
+}
+
+function cleanupAll() {
+ phridge.disposeAll();
+}
+
+/**
+ * This function is called whenever a resource is requested by PhantomJS.
+ * If we are loading either raw HTML or a local page, PhantomJS needs to be able to find the
+ * resource with an absolute path.
+ * There are two possible cases:
+ * - 'file://': This might be either a protocol-less URL or a relative path. Since we
+ * can't handle both, we choose to handle the former.
+ * - 'file:///': This is an absolute path. If options.htmlroot is specified, we have a chance to
+ * redirect the request to the correct location.
+ */
+function ResourceHandler(htmlroot, isWindows, resolve) {
+ var ignoredExtensions = ['\\.css', '\\.png', '\\.gif', '\\.jpg', '\\.jpeg', ''],
+ ignoredEndpoints = ['fonts\\.googleapis'];
+
+ var ignoreRequests = new RegExp(ignoredExtensions.join('$|') + ignoredEndpoints.join('|'));
+
+ this.onResourceRequested = function (requestData, networkRequest) {
+ var originalUrl = requestData.url,
+ url = originalUrl.split('?')[0].split('#')[0];
+
+ if (url.substr(-3) === '.js' && url.substr(0, 7) === 'file://') {
+ /* Try and match protocol-less URLs and absolute ones.
+ * Relative URLs will still not load.
+ */
+ if (url.substr(5, 3) === '///') {
+ /* Absolute URL
+ * Retry loading the resource appending the htmlroot option
+ */
+ if (isWindows) {
+ /* Do not strip leading '/' */
+ url = originalUrl.substr(0, 8) + htmlroot + originalUrl.substr(7);
+ } else {
+ url = originalUrl.substr(0, 7) + htmlroot + originalUrl.substr(7);
+ }
+ } else {
+ /* Protocol-less URL */
+ url = 'http://' + originalUrl.substr(7);
+ }
+ networkRequest.changeUrl(url);
+ } else if (ignoreRequests.test(url)) {
+ networkRequest.abort();
+ }
+ };
+ resolve();
+}
+
+/**
+ * Helper for fromRaw, fromLocal, fromRemote;
+ * return the phantom page after the timeout
+ * has elapsed
+ * @param {phantom} page Page created by phantom
+ * @param {Object} options
+ * @return {promise}
+ */
+function resolveWithPage(page, options) {
+ return function () {
+ return new promise(function (resolve) {
+ setTimeout(function () {
+ return resolve(page);
+ }, options.timeout);
+ });
+ };
+}
+
+/**
+ * Load a page given an HTML string.
+ * @param {String} html
+ * @param {Object} options
+ * @return {promise}
+ */
+function fromRaw(html, options) {
+ var page = phantom.createPage(),
+ htmlroot = path.join(process.cwd(), options.htmlroot || '');
+
+ return promise.resolve(page.run(htmlroot, utility.isWindows(), ResourceHandler).then(function () {
+ return page.run(html, function (raw) {
+ this.setContent(raw, 'local');
+ });
+ }).then(resolveWithPage(page, options)));
+}
+
+/**
+ * Open a page given a filename.
+ * @param {String} filename
+ * @param {Object} options
+ * @return {promise}
+ */
+function fromLocal(filename, options) {
+ return promise.promisify(fs.readFile)(filename, 'utf-8').then(function (html) {
+ return fromRaw(html, options);
+ });
+}
+
+/**
+ * Open a page given a URL.
+ * @param {String} url
+ * @param {Object} options
+ * @return {promise}
+ */
+function fromRemote(url, options) {
+ /* If the protocol is unspecified, default to HTTP */
+ if (!/^http/.test(url)) {
+ url = 'http:' + url;
+ }
+
+ return promise.resolve(phantom.openPage(url).then(function (page) {
+ return resolveWithPage(page, options)();
+ }));
+}
+
+/**
+ * Extract stylesheets' hrefs from dom
+ * @param {Object} page A PhantomJS page
+ * @param {Object} options Options, as passed to UnCSS
+ * @return {promise}
+ */
+function getStylesheets(page, options) {
+ if (_.isArray(options.media) === false) {
+ options.media = [options.media];
+ }
+ var media = _.union(['', 'all', 'screen'], options.media);
+ return page.run(function () {
+ return this.evaluate(function () {
+ return Array.prototype.map.call(document.querySelectorAll('link[rel="stylesheet"]'), function (link) {
+ return {
+ href: link.href,
+ media: link.media
+ };
+ });
+ });
+ }).then(function (stylesheets) {
+ stylesheets = _
+ .toArray(stylesheets)
+ /* Match only specified media attributes, plus defaults */
+ .filter(function (sheet) {
+ return media.indexOf(sheet.media) !== -1;
+ })
+ .map(function (sheet) {
+ return sheet.href;
+ });
+ return stylesheets;
+ });
+}
+
+/**
+ * Filter unused selectors.
+ * @param {Object} page A PhantomJS page
+ * @param {Array} sels List of selectors to be filtered
+ * @return {promise}
+ */
+function findAll(page, sels) {
+ return page.run(sels, function (args) {
+ return this.evaluate(function (selectors) {
+ // Unwrap noscript elements
+ Array.prototype.forEach.call(document.getElementsByTagName('noscript'), function (ns) {
+ var wrapper = document.createElement('div');
+ wrapper.innerHTML = ns.innerText;
+ // Insert each child of the <noscript> as its sibling
+ Array.prototype.forEach.call(wrapper.children, function (child) {
+ ns.parentNode.insertBefore(child, ns);
+ });
+ });
+ // Do the filtering
+ selectors = selectors.filter(function (selector) {
+ try {
+ if (document.querySelector(selector)) {
+ return true;
+ }
+ } catch (e) {
+ return true;
+ }
+ return false;
+ });
+ return {
+ selectors: selectors
+ };
+ }, args);
+ }).then(function (res) {
+ if (res === null) {
+ return [];
+ }
+ return res.selectors;
+ });
+}
+
+module.exports = {
+ init: init,
+ cleanupAll: cleanupAll,
+ fromLocal: fromLocal,
+ fromRaw: fromRaw,
+ fromRemote: fromRemote,
+ findAll: findAll,
+ getStylesheets: getStylesheets
+};