Version 1
[yaffs-website] / node_modules / uncss / src / phantom.js
diff --git a/node_modules/uncss/src/phantom.js b/node_modules/uncss/src/phantom.js
new file mode 100644 (file)
index 0000000..31d4322
--- /dev/null
@@ -0,0 +1,238 @@
+'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
+};