@michaelheerklotz/dr-css-inliner
Version:
Puppeteer script to inline above-the-fold CSS for a webpage.
278 lines (230 loc) • 6.63 kB
JavaScript
(function (global, doc) {
var html = doc.documentElement;
var width, height;
var options = global.extractCSSOptions;
var matchMQ, required;
var stylesheets = [];
var mediaStylesheets = [];
var left;
var isRunning;
if (options) {
if ("matchMQ" in options) {
matchMQ = options.matchMQ;
}
if ("required" in options) {
required = options.required;
required = required.map(function (string) {
return new RegExp(string, "i");
});
}
}
function init(force) {
if (isRunning && !force) {
return;
}
isRunning = true;
var links = doc.querySelectorAll("link");
var cssLinksStillLoading = Array.prototype.slice.call(links).filter(function (link) {
if (link.rel == "preload" && link.as == "style") {
return true;
}
return false;
});
if (cssLinksStillLoading.length > 0) {
setTimeout(function () {
init(true);
}, 50);
return;
}
width = html.offsetWidth;
height = global.innerHeight;
mediaStylesheets = Array.prototype.slice.call(doc.styleSheets).filter(function (stylesheet) {
return (!stylesheet.media.length || stylesheet.media[0] == "screen" || stylesheet.media[0] == "all");
});
left = mediaStylesheets.slice(0);
var base = global.location.href.replace(/\/[^/]+$/, "/");
var host = global.location.protocol + "//" + global.location.host;
mediaStylesheets.forEach(function (stylesheet) {
if (stylesheet.href) {
fetchStylesheet(stylesheet.href, function (text) {
var index = left.indexOf(stylesheet);
if (index > -1) {
left.splice(index, 1);
}
text = text.replace(/\/\*[\s\S]+?\*\//g, "").replace(/[\n\r]+/g, "").replace(/url\((["']|)(\.\.\/[^"'\(\)]+)\1\)/g, function (m, quote, url) {
return "url(" + quote + pathRelativeToPage(base, stylesheet.href, url) + quote + ")";
});
text = text.replace(/@charset "[^"]*";/gm, "");
stylesheets.push(text);
if (left.length === 0) {
complete();
}
});
}
else {
var index = left.indexOf(stylesheet);
if (index > -1) {
left.splice(index, 1);
}
if (left.length === 0) {
complete();
}
}
});
function complete() {
var elements = false;
// if viewport height is forced
// define elements to check seletors against
if (html.scrollHeight != height) {
elements = Array.prototype.slice.call(doc.getElementsByTagName("*")).filter(function (element) {
return (element.getBoundingClientRect().top < height);
});
}
var CSS = stylesheets.map(function (css) {
return outputRules(filterCSS(css, elements));
}).join("");
console.log('_extractedcss', {css: CSS });
isRunning = false;
}
}
function outputRules(rules) {
return rules.map(function (rule) {
return rule.selectors.join(",") + "{" + rule.css + "}";
}).join("");
}
function matchSelectors(selectors, elements) {
return selectors.map(function (selector) {
// strip comments
return selector.replace(/\/\*[\s\S]+?\*\//g, "").replace(/(?:^\s+)|(?:\s+$)/g, "");
}).filter(function (selector) {
if (!selector || selector.match(/@/)) {
return false;
}
if (required) {
var found = required.some(function (reg) {
return reg.test(selector);
});
if (found) {
return true;
}
}
if (selector.indexOf(":") > -1) {
// strip pseudo-classes that may not be directly selectable or can change on userinteraction
selector = selector.replace(/(?:::?)(?:after|before|link|visited|hover|active|focus|selection|checked|selected|optional|required|invalid|valid|in-range|read-only|read-write|target|(?:-[a-zA-Z-]+))\s*$/g, "");
}
if (selector.length == 0) {
return true;
}
var matches = [];
try {
matches = doc.querySelectorAll(selector);
}
catch(e){}
var i = 0;
var l = matches.length;
if (l) {
if (elements) {
while (i < l) {
if (elements.indexOf(matches[i++]) > -1) {
return true;
}
}
return false;
}
return true;
}
return false;
});
}
function filterCSS(css, elements) {
var rules = parseRules(css),
matchedRules = [];
rules.forEach(function (rule) {
var matchingSelectors = [];
var atRuleMatch = rule.selectors[0].match(/^\s*(@[a-z\-]+)/);
if (rule.selectors) {
if (atRuleMatch) {
switch (atRuleMatch[1]) {
case "@font-face":
matchingSelectors = ["@font-face"];
break;
case "@media":
var mq;
if (matchMQ) {
var widths = rule.selectors[0].match(/m(?:ax|in)-width:[^)]+/g);
var pair;
if (widths) {
mq = {};
while (widths.length) {
pair = widths.shift().split(/:\s?/);
mq[pair[0]] = parseInt(pair[1]);
}
}
}
if (!matchMQ || !mq || ((!("min-width" in mq) || mq["min-width"] <= width) && (!("max-width" in mq) || mq["max-width"] >= width))) {
var matchedSubRules = filterCSS(rule.css, elements);
if (matchedSubRules.length) {
matchingSelectors = rule.selectors;
rule.css = outputRules(matchedSubRules);
}
}
break;
}
}
else {
matchingSelectors = matchSelectors(rule.selectors, elements);
}
}
if (matchingSelectors.length) {
rule.selectors = matchingSelectors;
matchedRules.push(rule);
}
});
return matchedRules;
}
function parseRules(css) {
var matches = css.replace(/\n+/g, " ").match(/(?:[^{}]+\s*\{[^{}]+\})|(?:[^{}]+\{\s*(?:[^{}]+\{[^{}]+\})+\s*\})/g);
var rules = [];
if (matches) {
matches.forEach(function (match) {
var rule = parseRule(match);
if (rule) {
rules.push(rule);
}
});
}
return rules;
}
function parseRule(rule) {
var match = rule.match(/^,*\s*([^{}]+)\s*\{\s*((?:[^{}]+\{[^{}]+\})+|[^{}]+)\s*\}$/);
return {
selectors: match && match[1].split(/\s*,\s*(?![^\(\[]*[\]\)])/),
css: match && match[2]
};
}
function pathRelativeToPage(basepath, csspath, sourcepath) {
while (sourcepath.indexOf("../") === 0) {
sourcepath = sourcepath.slice(3);
csspath = csspath.replace(/\/[^/]+\/[^/]*$/, "/");
}
var path = csspath + sourcepath;
return (path.indexOf(basepath) === 0) ? path.slice(basepath.length) : path;
}
function fetchStylesheet(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.onload = function () {
callback(xhr.responseText);
};
xhr.send(null);
return xhr;
}
if (doc.readyState != "complete") {
global.addEventListener("load", function () {
init();
}, false);
}
else {
init();
}
}(this, document));