2014-08-04 00:01:39 +02:00
|
|
|
/**
|
|
|
|
* Module to inline styles while loading the existing stylesheets async
|
|
|
|
*
|
|
|
|
* @author Ben Zörb @bezoerb https://github.com/bezoerb
|
|
|
|
* @copyright Copyright (c) 2014 Ben Zörb
|
|
|
|
*
|
|
|
|
* Licensed under the MIT license.
|
|
|
|
* http://bezoerb.mit-license.org/
|
|
|
|
* All rights reserved.
|
|
|
|
*/
|
2019-08-08 18:10:41 +02:00
|
|
|
|
2014-08-04 00:01:39 +02:00
|
|
|
'use strict';
|
2019-08-08 18:10:41 +02:00
|
|
|
|
2017-03-20 22:42:19 +01:00
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
2019-11-03 14:23:08 +01:00
|
|
|
const isString = require('lodash.isstring');
|
|
|
|
const isRegExp = require('lodash.isregexp');
|
2017-03-20 22:42:19 +01:00
|
|
|
const reaver = require('reaver');
|
|
|
|
const slash = require('slash');
|
2018-12-30 23:41:29 +01:00
|
|
|
|
|
|
|
const Dom = require('./src/dom');
|
|
|
|
const {prettifyCss, extractCss} = require('./src/css');
|
2016-04-14 17:26:33 +02:00
|
|
|
|
2018-12-20 07:18:51 +01:00
|
|
|
const DEFAULT_OPTIONS = {
|
|
|
|
minify: true,
|
|
|
|
extract: false,
|
2020-05-20 00:24:00 +02:00
|
|
|
polyfill: false,
|
|
|
|
preload: false,
|
2018-12-20 07:18:51 +01:00
|
|
|
ignore: [],
|
2019-01-08 05:33:19 +01:00
|
|
|
replaceStylesheets: false,
|
2019-03-26 17:31:54 +01:00
|
|
|
noscript: 'body',
|
2018-12-20 07:18:51 +01:00
|
|
|
};
|
|
|
|
|
2014-11-25 17:21:43 +01:00
|
|
|
/**
|
|
|
|
* Fixup slashes in file paths for windows
|
2016-04-14 22:36:45 +02:00
|
|
|
*
|
2018-12-18 12:51:30 +01:00
|
|
|
* @param {string} str Filepath
|
|
|
|
* @return {string} Normalized path
|
2014-11-25 17:21:43 +01:00
|
|
|
*/
|
2020-05-23 00:36:56 +02:00
|
|
|
function normalizePath(string) {
|
|
|
|
return process.platform === 'win32' ? slash(string) : string;
|
2014-11-25 17:21:43 +01:00
|
|
|
}
|
2014-08-04 00:01:39 +02:00
|
|
|
|
2018-12-18 12:51:30 +01:00
|
|
|
/**
|
|
|
|
* Main function ;)
|
|
|
|
* @param {string} html HTML String
|
|
|
|
* @param {string} styles CSS String
|
|
|
|
* @param {object} options Options
|
|
|
|
* @returns {string} HTML Source with inlined critical css
|
|
|
|
*/
|
|
|
|
function inline(html, styles, options) {
|
2018-12-20 07:18:51 +01:00
|
|
|
const o = {...DEFAULT_OPTIONS, ...(options || {})};
|
|
|
|
|
|
|
|
if (!isString(html)) {
|
2018-12-18 12:51:30 +01:00
|
|
|
html = String(html);
|
|
|
|
}
|
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
if (!Array.isArray(o.ignore)) {
|
2020-05-23 00:36:56 +02:00
|
|
|
o.ignore = [o.ignore].filter((i) => i);
|
2018-12-30 23:41:29 +01:00
|
|
|
}
|
2018-12-18 12:51:30 +01:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
const document = new Dom(html, o);
|
2018-12-19 22:32:55 +01:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
const inlineStyles = document.getInlineStyles();
|
2019-11-13 14:58:05 +01:00
|
|
|
const externalStyles = document.getExternalStyles();
|
2018-12-30 23:41:29 +01:00
|
|
|
const missingStyles = extractCss(styles, ...inlineStyles);
|
2018-12-20 07:18:51 +01:00
|
|
|
|
2020-05-23 00:36:56 +02:00
|
|
|
const links = externalStyles.filter((link) => {
|
2018-12-30 23:41:29 +01:00
|
|
|
// Only take stylesheets
|
|
|
|
const stylesheet = link.getAttribute('rel') === 'stylesheet';
|
|
|
|
// Filter ignored links
|
|
|
|
const href = link.getAttribute('href');
|
2020-05-23 00:36:56 +02:00
|
|
|
return stylesheet && !o.ignore.some((i) => (isRegExp(i) && i.test(href)) || i === href);
|
2018-12-18 12:51:30 +01:00
|
|
|
});
|
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
const targetSelectors = [
|
|
|
|
o.selector,
|
|
|
|
':not(noscript) > link[rel="stylesheet"]',
|
|
|
|
':not(noscript) > link[rel="preload"][as="style"]',
|
|
|
|
'head script',
|
|
|
|
];
|
2018-12-18 12:51:30 +01:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
const target = document.querySelector(targetSelectors);
|
|
|
|
const inlined = `${inlineStyles}\n${missingStyles}`;
|
2018-12-18 12:51:30 +01:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
if (missingStyles) {
|
|
|
|
if (o.minify) {
|
|
|
|
document.addInlineStyles(missingStyles, target);
|
2018-12-18 12:51:30 +01:00
|
|
|
} else {
|
2018-12-30 23:41:29 +01:00
|
|
|
document.addInlineStyles(prettifyCss(missingStyles, document.indent), target);
|
2016-04-18 06:35:08 +02:00
|
|
|
}
|
2018-12-18 12:51:30 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 23:57:15 +01:00
|
|
|
if (Array.isArray(o.replaceStylesheets) && links.length > 0) {
|
2018-12-30 23:41:29 +01:00
|
|
|
// Detect links to be removed
|
|
|
|
const [ref] = links;
|
|
|
|
const removable = [...document.querySelectorAll('link[rel="stylesheet"], link[rel="preload"][as="style"]')].filter(
|
2020-05-23 00:36:56 +02:00
|
|
|
(link) => {
|
2018-12-30 23:41:29 +01:00
|
|
|
// Filter ignored links
|
|
|
|
const href = link.getAttribute('href');
|
2020-05-23 00:36:56 +02:00
|
|
|
return !o.ignore.some((i) => (isRegExp(i) && i.test(href)) || i === href);
|
2018-12-18 12:51:30 +01:00
|
|
|
}
|
2018-12-30 23:41:29 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
// Add link tags before old links
|
|
|
|
// eslint-disable-next-line array-callback-return
|
2020-05-23 00:36:56 +02:00
|
|
|
o.replaceStylesheets.map((href) => {
|
2018-12-30 23:41:29 +01:00
|
|
|
const link = document.createElement('link');
|
2019-08-08 18:10:41 +02:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
link.setAttribute('rel', 'stylesheet');
|
|
|
|
link.setAttribute('href', href);
|
2019-01-02 07:14:03 +01:00
|
|
|
document.addNoscript(link);
|
2018-12-18 12:51:30 +01:00
|
|
|
|
2020-05-20 00:24:00 +02:00
|
|
|
if (o.polyfill) {
|
|
|
|
link.setAttribute('rel', 'preload');
|
|
|
|
link.setAttribute('as', 'style');
|
|
|
|
link.setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
|
|
|
|
} else {
|
|
|
|
link.setAttribute('rel', 'stylesheet');
|
|
|
|
link.setAttribute('media', 'print');
|
|
|
|
link.setAttribute('onload', "this.media='all'");
|
|
|
|
}
|
2018-12-18 12:51:30 +01:00
|
|
|
|
2020-05-23 00:36:56 +02:00
|
|
|
ref.before(link);
|
2020-05-20 00:24:00 +02:00
|
|
|
|
|
|
|
if (!o.polyfill && o.preload) {
|
|
|
|
const preload = document.createElement('link');
|
|
|
|
preload.setAttribute('rel', 'preload');
|
|
|
|
preload.setAttribute('href', link.getAttribute('href'));
|
|
|
|
preload.setAttribute('as', 'style');
|
|
|
|
|
2020-05-23 00:36:56 +02:00
|
|
|
link.before(preload);
|
2020-05-20 00:24:00 +02:00
|
|
|
}
|
2018-12-30 23:41:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
// Remove old links
|
|
|
|
// eslint-disable-next-line array-callback-return
|
2020-05-23 00:36:56 +02:00
|
|
|
removable.map((link) => {
|
2018-12-30 23:41:29 +01:00
|
|
|
if (link.parentElement.tagName === 'NOSCRIPT') {
|
|
|
|
document.remove(link.parentElement);
|
|
|
|
} else {
|
|
|
|
document.remove(link);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Modify links and add clones to noscript block
|
|
|
|
// eslint-disable-next-line array-callback-return
|
2020-05-23 00:36:56 +02:00
|
|
|
links.map((link) => {
|
2018-12-18 12:51:30 +01:00
|
|
|
if (o.extract) {
|
2018-12-30 23:41:29 +01:00
|
|
|
const href = link.getAttribute('href');
|
2020-08-22 22:09:30 +02:00
|
|
|
const fileOrig = path.resolve(path.join(o.basePath || process.cwd, href));
|
|
|
|
const files = [fileOrig, fileOrig.replace(/\?.*/, '')];
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
if (fs.existsSync(file)) {
|
|
|
|
const orig = fs.readFileSync(file);
|
|
|
|
const diff = extractCss(orig, inlined, o.minify);
|
|
|
|
const filename = reaver.rev(file, diff);
|
|
|
|
|
|
|
|
fs.writeFileSync(filename, diff);
|
|
|
|
link.setAttribute('href', normalizePath(reaver.rev(href, diff)));
|
|
|
|
// eslint-disable-next-line array-callback-return
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!/\/\//.test(href)) {
|
2018-12-30 23:41:29 +01:00
|
|
|
throw new Error(`Error: file "${href}" not found in "${o.basePath || process.cwd}". Specify base path.`);
|
2018-12-18 12:51:30 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-29 17:35:58 +01:00
|
|
|
|
2019-01-02 07:14:03 +01:00
|
|
|
document.addNoscript(link);
|
2015-07-10 21:55:28 +02:00
|
|
|
|
2020-05-20 00:24:00 +02:00
|
|
|
if (o.polyfill) {
|
|
|
|
link.setAttribute('rel', 'preload');
|
|
|
|
link.setAttribute('as', 'style');
|
|
|
|
link.setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
|
|
|
|
} else {
|
|
|
|
link.setAttribute('rel', 'stylesheet');
|
|
|
|
link.setAttribute('media', 'print');
|
|
|
|
link.setAttribute('onload', "this.media='all'");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!o.polyfill && o.preload) {
|
|
|
|
const preload = document.createElement('link');
|
|
|
|
preload.setAttribute('href', link.getAttribute('href'));
|
|
|
|
preload.setAttribute('rel', 'preload');
|
|
|
|
preload.setAttribute('as', 'style');
|
2020-05-23 00:36:56 +02:00
|
|
|
link.before(preload);
|
2020-05-20 00:24:00 +02:00
|
|
|
}
|
2015-11-13 22:59:10 +01:00
|
|
|
});
|
2018-12-18 12:51:30 +01:00
|
|
|
}
|
2014-10-19 03:12:33 +02:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
// Add loadcss if it's not already loaded
|
|
|
|
if (o.polyfill) {
|
|
|
|
document.maybeAddLoadcss();
|
|
|
|
}
|
2015-11-13 22:59:10 +01:00
|
|
|
|
2018-12-30 23:41:29 +01:00
|
|
|
return Buffer.from(document.serialize());
|
2018-12-18 12:51:30 +01:00
|
|
|
}
|
2015-02-17 00:59:53 +01:00
|
|
|
|
2018-12-18 12:51:30 +01:00
|
|
|
module.exports = inline;
|