You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
4.0 KiB
160 lines
4.0 KiB
'use strict'; |
|
|
|
exports.type = 'full'; |
|
|
|
exports.active = true; |
|
|
|
exports.description = 'minifies styles and removes unused styles based on usage data'; |
|
|
|
exports.params = { |
|
// ... CSSO options goes here |
|
|
|
// additional |
|
usage: { |
|
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes) |
|
ids: true, |
|
classes: true, |
|
tags: true |
|
} |
|
}; |
|
|
|
var csso = require('csso'); |
|
|
|
/** |
|
* Minifies styles (<style> element + style attribute) using CSSO |
|
* |
|
* @author strarsis <strarsis@gmail.com> |
|
*/ |
|
exports.fn = function(ast, options) { |
|
options = options || {}; |
|
|
|
var minifyOptionsForStylesheet = cloneObject(options); |
|
var minifyOptionsForAttribute = cloneObject(options); |
|
var elems = findStyleElems(ast); |
|
|
|
minifyOptionsForStylesheet.usage = collectUsageData(ast, options); |
|
minifyOptionsForAttribute.usage = null; |
|
|
|
elems.forEach(function(elem) { |
|
if (elem.isElem('style')) { |
|
// <style> element |
|
var styleCss = elem.content[0].text || elem.content[0].cdata || []; |
|
var DATA = styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0 ? 'cdata' : 'text'; |
|
|
|
elem.content[0][DATA] = csso.minify(styleCss, minifyOptionsForStylesheet).css; |
|
} else { |
|
// style attribute |
|
var elemStyle = elem.attr('style').value; |
|
|
|
elem.attr('style').value = csso.minifyBlock(elemStyle, minifyOptionsForAttribute).css; |
|
} |
|
}); |
|
|
|
return ast; |
|
}; |
|
|
|
function cloneObject(obj) { |
|
var result = {}; |
|
|
|
for (var key in obj) { |
|
result[key] = obj[key]; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
function findStyleElems(ast) { |
|
|
|
function walk(items, styles) { |
|
for (var i = 0; i < items.content.length; i++) { |
|
var item = items.content[i]; |
|
|
|
// go deeper |
|
if (item.content) { |
|
walk(item, styles); |
|
} |
|
|
|
if (item.isElem('style') && !item.isEmpty()) { |
|
styles.push(item); |
|
} else if (item.isElem() && item.hasAttr('style')) { |
|
styles.push(item); |
|
} |
|
} |
|
|
|
return styles; |
|
} |
|
|
|
return walk(ast, []); |
|
} |
|
|
|
function shouldFilter(options, name) { |
|
if ('usage' in options === false) { |
|
return true; |
|
} |
|
|
|
if (options.usage && name in options.usage === false) { |
|
return true; |
|
} |
|
|
|
return Boolean(options.usage && options.usage[name]); |
|
} |
|
|
|
function collectUsageData(ast, options) { |
|
|
|
function walk(items, usageData) { |
|
for (var i = 0; i < items.content.length; i++) { |
|
var item = items.content[i]; |
|
|
|
// go deeper |
|
if (item.content) { |
|
walk(item, usageData); |
|
} |
|
|
|
if (item.isElem('script')) { |
|
safe = false; |
|
} |
|
|
|
if (item.isElem()) { |
|
usageData.tags[item.elem] = true; |
|
|
|
if (item.hasAttr('id')) { |
|
usageData.ids[item.attr('id').value] = true; |
|
} |
|
|
|
if (item.hasAttr('class')) { |
|
item.attr('class').value.replace(/^\s+|\s+$/g, '').split(/\s+/).forEach(function(className) { |
|
usageData.classes[className] = true; |
|
}); |
|
} |
|
|
|
if (item.attrs && Object.keys(item.attrs).some(function(name) { return /^on/i.test(name); })) { |
|
safe = false; |
|
} |
|
} |
|
} |
|
|
|
return usageData; |
|
} |
|
|
|
var safe = true; |
|
var usageData = {}; |
|
var hasData = false; |
|
var rawData = walk(ast, { |
|
ids: Object.create(null), |
|
classes: Object.create(null), |
|
tags: Object.create(null) |
|
}); |
|
|
|
if (!safe && options.usage && options.usage.force) { |
|
safe = true; |
|
} |
|
|
|
for (var key in rawData) { |
|
if (shouldFilter(options, key)) { |
|
usageData[key] = Object.keys(rawData[key]); |
|
hasData = true; |
|
} |
|
} |
|
|
|
return safe && hasData ? usageData : null; |
|
}
|
|
|