csso
Advanced tools
Comparing version 1.7.1 to 1.8.0
@@ -0,1 +1,16 @@ | ||
## 1.8.0 (March 24, 2016) | ||
- Usage data support: | ||
- Filter rulesets by tag names, class names and ids white lists. | ||
- More aggressive ruleset moving using class name scopes information. | ||
- New CLI option `--usage` to pass usage data file. | ||
- Improve initial ruleset merge | ||
- Change order of ruleset processing, now it's left to right. Previously unmerged rulesets may prevent lookup and other rulesets merge. | ||
- Difference in pseudo signature just prevents ruleset merging, but don't stop lookup. | ||
- Simplify block comparison (performance). | ||
- New method `csso.minifyBlock()` for css block compression (e.g. `style` attribute content). | ||
- Ruleset merge improvement: at-rules with block (like `@media` or `@supports`) now can be skipped during ruleset merge lookup if doesn't contain something prevents it. | ||
- FIX: Add negation (`:not()`) to pseudo signature to avoid unsafe merge (old browsers doesn't support it). | ||
- FIX: Check nested parts of value when compute compatibility. It fixes unsafe property merging. | ||
## 1.7.1 (March 16, 2016) | ||
@@ -93,3 +108,3 @@ | ||
- `outputAst` – new option to specify output AST format (`gonzales` by default for backward compatibility) | ||
- remove quotes surrounding attribute values in attribute selectors when possible (issue #73) | ||
- remove quotes surrounding attribute values in attribute selectors when possible (#73) | ||
- replace `from`→`0%` and `100%`→`to` at `@keyframes` (#205) | ||
@@ -209,8 +224,8 @@ - prevent partial merge of rulesets at `@keyframes` (#80, #197) | ||
- Case insensitive check for `!important` (issue #187) | ||
- Fix problems with using `csso` as cli command on Windows (issue #83, #136, #142 and others) | ||
- Case insensitive check for `!important` (#187) | ||
- Fix problems with using `csso` as cli command on Windows (#83, #136, #142 and others) | ||
- Remove byte order marker (the UTF-8 BOM) from input | ||
- Don't strip space between funktion-funktion and funktion-vhash (issue #134) | ||
- Don't merge TRBL values having \9 (hack for IE8 in bootstrap) (issues #159, #214, #230, #231 and others) | ||
- Don't strip units off dimensions of non-length (issues #226, #229 and others) | ||
- Don't strip space between funktion-funktion and funktion-vhash (#134) | ||
- Don't merge TRBL values having \9 (hack for IE8 in bootstrap) (#159, #214, #230, #231 and others) | ||
- Don't strip units off dimensions of non-length (#226, #229 and others) | ||
@@ -217,0 +232,0 @@ ## 1.3.7 (February 11, 2013) |
@@ -180,4 +180,5 @@ var fs = require('fs'); | ||
.option('-o, --output <filename>', 'Output file (result outputs to stdout if not set)') | ||
.option('-m, --map <destination>', 'Generate source map. Possible values: none (default), inline, file or <filename>', 'none') | ||
.option('--input-map <source>', 'Input source map. Possible values: none, auto (default) or <filename>', 'auto') | ||
.option('-m, --map <destination>', 'Generate source map: none (default), inline, file or <filename>', 'none') | ||
.option('-u, --usage <filenane>', 'Usage data file') | ||
.option('--input-map <source>', 'Input source map: none, auto (default) or <filename>', 'auto') | ||
.option('--restructure-off', 'Turns structure minimization off') | ||
@@ -190,2 +191,4 @@ .option('--stat', 'Output statistics in stderr') | ||
var outputFile = options.output || args[1]; | ||
var usageFile = options.usage; | ||
var usageData = false; | ||
var map = options.map; | ||
@@ -215,2 +218,18 @@ var inputMap = options.inputMap; | ||
if (usageFile) { | ||
if (!fs.existsSync(usageFile)) { | ||
console.error('Usage data file doesn\'t found (%s)', usageFile); | ||
process.exit(2); | ||
} | ||
usageData = fs.readFileSync(usageFile, 'utf-8'); | ||
try { | ||
usageData = JSON.parse(usageData); | ||
} catch (e) { | ||
console.error('Usage data parse error (%s)', usageFile); | ||
process.exit(2); | ||
} | ||
} | ||
readFromStream(inputStream, function(source) { | ||
@@ -228,2 +247,3 @@ var time = process.hrtime(); | ||
sourceMap: sourceMap.output, | ||
usage: usageData, | ||
restructure: !structureOptimisationOff, | ||
@@ -230,0 +250,0 @@ debug: debug |
@@ -1,2 +0,2 @@ | ||
module.exports = function(node, item, list) { | ||
module.exports = function cleanIdentifier(node, item, list) { | ||
// remove useless universal selector | ||
@@ -3,0 +3,0 @@ if (this.selector !== null && node.name === '*') { |
@@ -0,1 +1,2 @@ | ||
var walk = require('../ast/walk.js').all; | ||
var handlers = { | ||
@@ -9,6 +10,8 @@ Space: require('./Space.js'), | ||
module.exports = function(node, item, list) { | ||
if (handlers.hasOwnProperty(node.type)) { | ||
handlers[node.type].call(this, node, item, list); | ||
} | ||
module.exports = function(ast, usageData) { | ||
walk(ast, function(node, item, list) { | ||
if (handlers.hasOwnProperty(node.type)) { | ||
handlers[node.type].call(this, node, item, list, usageData); | ||
} | ||
}); | ||
}; |
@@ -1,2 +0,35 @@ | ||
module.exports = function cleanRuleset(node, item, list) { | ||
var hasOwnProperty = Object.prototype.hasOwnProperty; | ||
function cleanUnused(node, usageData) { | ||
return node.selector.selectors.each(function(selector, item, list) { | ||
var hasUnused = selector.sequence.some(function(node) { | ||
switch (node.type) { | ||
case 'Class': | ||
return usageData.classes && !hasOwnProperty.call(usageData.classes, node.name); | ||
case 'Id': | ||
return usageData.ids && !hasOwnProperty.call(usageData.ids, node.name); | ||
case 'Identifier': | ||
// ignore universal selector | ||
if (node.name !== '*') { | ||
// TODO: remove toLowerCase when type selectors will be normalized | ||
return usageData.tags && !hasOwnProperty.call(usageData.tags, node.name.toLowerCase()); | ||
} | ||
break; | ||
} | ||
}); | ||
if (hasUnused) { | ||
list.remove(item); | ||
} | ||
}); | ||
} | ||
module.exports = function cleanRuleset(node, item, list, usageData) { | ||
if (usageData) { | ||
cleanUnused(node, usageData); | ||
} | ||
if (node.selector.selectors.isEmpty() || | ||
@@ -3,0 +36,0 @@ node.block.declarations.isEmpty()) { |
@@ -0,1 +1,2 @@ | ||
var walk = require('../ast/walk.js').all; | ||
var handlers = { | ||
@@ -15,6 +16,8 @@ Atrule: require('./Atrule.js'), | ||
module.exports = function(node, item, list) { | ||
if (handlers.hasOwnProperty(node.type)) { | ||
handlers[node.type].call(this, node, item, list); | ||
} | ||
module.exports = function(ast) { | ||
walk(ast, function(node, item, list) { | ||
if (handlers.hasOwnProperty(node.type)) { | ||
handlers[node.type].call(this, node, item, list); | ||
} | ||
}); | ||
}; |
@@ -1,8 +0,8 @@ | ||
var List = require('../utils/list.js'); | ||
var convertToInternal = require('./ast/gonzalesToInternal.js'); | ||
var convertToGonzales = require('./ast/internalToGonzales.js'); | ||
var internalWalkAll = require('./ast/walk.js').all; | ||
var List = require('../utils/list'); | ||
var usageUtils = require('./usage'); | ||
var convertToInternal = require('./ast/gonzalesToInternal'); | ||
var convertToGonzales = require('./ast/internalToGonzales'); | ||
var clean = require('./clean'); | ||
var compress = require('./compress'); | ||
var restructureAst = require('./restructure'); | ||
var restructureBlock = require('./restructure'); | ||
@@ -52,3 +52,3 @@ function injectInfo(token) { | ||
function compressBlock(ast, restructuring, num, logger) { | ||
function compressBlock(ast, usageData, num, logger) { | ||
logger('Compress block #' + num, null, true); | ||
@@ -61,15 +61,10 @@ | ||
// remove useless | ||
internalWalkAll(internalAst, clean); | ||
// remove redundant | ||
clean(internalAst, usageData); | ||
logger('clean', internalAst); | ||
// compress nodes | ||
internalWalkAll(internalAst, compress); | ||
compress(internalAst, usageData); | ||
logger('compress', internalAst); | ||
// structure optimisations | ||
if (restructuring) { | ||
restructureAst(internalAst, logger); | ||
} | ||
return internalAst; | ||
@@ -79,3 +74,2 @@ } | ||
module.exports = function compress(ast, options) { | ||
ast = ast || [{}, 'stylesheet']; | ||
options = options || {}; | ||
@@ -93,3 +87,7 @@ | ||
var blockRules; | ||
var blockMode = false; | ||
var usageData = false; | ||
ast = ast || [{}, 'stylesheet']; | ||
if (typeof ast[0] === 'string') { | ||
@@ -99,6 +97,27 @@ injectInfo([ast]); | ||
if (ast[1] !== 'stylesheet') { | ||
blockMode = true; | ||
ast = [null, 'stylesheet', | ||
[null, 'ruleset', | ||
[null, 'selector', | ||
[null, 'simpleselector', [null, 'ident', 'x']]], | ||
ast | ||
] | ||
]; | ||
} | ||
if (options.usage) { | ||
usageData = usageUtils.buildIndex(options.usage); | ||
} | ||
do { | ||
block = readBlock(ast, block.offset); | ||
block.stylesheet.firstAtrulesAllowed = firstAtrulesAllowed; | ||
block.stylesheet = compressBlock(block.stylesheet, restructuring, blockNum++, logger); | ||
block.stylesheet = compressBlock(block.stylesheet, usageData, blockNum++, logger); | ||
// structure optimisations | ||
if (restructuring) { | ||
restructureBlock(block.stylesheet, usageData, logger); | ||
} | ||
blockRules = block.stylesheet.rules; | ||
@@ -141,13 +160,16 @@ | ||
if (!options.outputAst || options.outputAst === 'gonzales') { | ||
return convertToGonzales({ | ||
if (blockMode) { | ||
result = result.first().block; | ||
} else { | ||
result = { | ||
type: 'StyleSheet', | ||
rules: result | ||
}); | ||
}; | ||
} | ||
return { | ||
type: 'StyleSheet', | ||
rules: result | ||
}; | ||
if (!options.outputAst || options.outputAst === 'gonzales') { | ||
return convertToGonzales(result); | ||
} | ||
return result; | ||
}; |
var utils = require('./utils.js'); | ||
module.exports = function initialMergeRuleset(node, item, list) { | ||
var selector = node.selector.selectors; | ||
var block = node.block; | ||
var selectors = node.selector.selectors; | ||
var declarations = node.block.declarations; | ||
list.prevUntil(item.prev, function(prev) { | ||
// skip non-ruleset node if safe | ||
if (prev.type !== 'Ruleset') { | ||
return true; | ||
return utils.unsafeToSkipNode.call(selectors, prev); | ||
} | ||
if (node.pseudoSignature !== prev.pseudoSignature) { | ||
return true; | ||
} | ||
var prevSelectors = prev.selector.selectors; | ||
var prevDeclarations = prev.block.declarations; | ||
var prevSelector = prev.selector.selectors; | ||
var prevBlock = prev.block; | ||
// try to join rulesets with equal pseudo signature | ||
if (node.pseudoSignature === prev.pseudoSignature) { | ||
// try to join by selectors | ||
if (utils.isEqualLists(prevSelectors, selectors)) { | ||
prevDeclarations.appendList(declarations); | ||
list.remove(item); | ||
return true; | ||
} | ||
// try to join by selectors | ||
if (utils.isEqualLists(prevSelector, selector)) { | ||
prevBlock.declarations.appendList(block.declarations); | ||
list.remove(item); | ||
return true; | ||
// try to join by declarations | ||
if (utils.isEqualLists(declarations, prevDeclarations)) { | ||
utils.addSelectors(prevSelectors, selectors); | ||
list.remove(item); | ||
return true; | ||
} | ||
} | ||
// try to join by properties | ||
var diff = utils.compareDeclarations(block.declarations, prevBlock.declarations); | ||
if (!diff.ne1.length && !diff.ne2.length) { | ||
utils.addToSelector(prevSelector, selector); | ||
list.remove(item); | ||
return true; | ||
} | ||
// go to next ruleset if simpleselectors has no equal specificity and element selector | ||
return selector.some(function(a) { | ||
return prevSelector.some(function(b) { | ||
return a.compareMarker === b.compareMarker; | ||
}); | ||
}); | ||
// go to prev ruleset if has no selector similarities | ||
return utils.hasSimilarSelectors(selectors, prevSelectors); | ||
}); | ||
}; |
@@ -383,3 +383,3 @@ var List = require('../../utils/list.js'); | ||
module.exports = function restructBlock(ast, declarationMarker) { | ||
module.exports = function restructBlock(ast, indexer) { | ||
var stylesheetMap = {}; | ||
@@ -417,3 +417,3 @@ var shortDeclarations = []; | ||
processShorthands(shortDeclarations, declarationMarker); | ||
processShorthands(shortDeclarations, indexer.declaration); | ||
}; |
@@ -70,4 +70,10 @@ var resolveProperty = require('../ast/names.js').property; | ||
declaration.value.sequence.each(function(node) { | ||
declaration.value.sequence.each(function walk(node) { | ||
switch (node.type) { | ||
case 'Argument': | ||
case 'Value': | ||
case 'Braces': | ||
node.sequence.each(walk); | ||
break; | ||
case 'Identifier': | ||
@@ -109,2 +115,6 @@ var name = node.name; | ||
special[name + '()'] = true; | ||
// check nested tokens too | ||
node.arguments.each(walk); | ||
break; | ||
@@ -111,0 +121,0 @@ |
var utils = require('./utils.js'); | ||
/* | ||
At this step all rules has single simple selector. We try to join by equal | ||
declaration blocks to first rule, e.g. | ||
.a { color: red } | ||
b { ... } | ||
.b { color: red } | ||
-> | ||
.a, .b { color: red } | ||
b { ... } | ||
*/ | ||
module.exports = function mergeRuleset(node, item, list) { | ||
var selector = node.selector.selectors; | ||
var block = node.block.declarations; | ||
var nodeCompareMarker = selector.first().compareMarker; | ||
var selectors = node.selector.selectors; | ||
var declarations = node.block.declarations; | ||
var nodeCompareMarker = selectors.first().compareMarker; | ||
var skippedCompareMarkers = {}; | ||
list.nextUntil(item.next, function(next, nextItem) { | ||
// skip non-ruleset node if safe | ||
if (next.type !== 'Ruleset') { | ||
return true; | ||
return utils.unsafeToSkipNode.call(selectors, next); | ||
} | ||
@@ -19,3 +32,3 @@ | ||
var nextFirstSelector = next.selector.selectors.head; | ||
var nextBlock = next.block.declarations; | ||
var nextDeclarations = next.block.declarations; | ||
var nextCompareMarker = nextFirstSelector.data.compareMarker; | ||
@@ -29,5 +42,5 @@ | ||
// try to join by selectors | ||
if (selector.head === selector.tail) { | ||
if (selector.first().id === nextFirstSelector.data.id) { | ||
block.appendList(nextBlock); | ||
if (selectors.head === selectors.tail) { | ||
if (selectors.first().id === nextFirstSelector.data.id) { | ||
declarations.appendList(nextDeclarations); | ||
list.remove(nextItem); | ||
@@ -39,14 +52,10 @@ return; | ||
// try to join by properties | ||
if (utils.isEqualDeclarations(block, nextBlock)) { | ||
if (utils.isEqualDeclarations(declarations, nextDeclarations)) { | ||
var nextStr = nextFirstSelector.data.id; | ||
selector.some(function(data, item) { | ||
selectors.some(function(data, item) { | ||
var curStr = data.id; | ||
if (nextStr === curStr) { | ||
return true; | ||
} | ||
if (nextStr < curStr) { | ||
selector.insert(nextFirstSelector, item); | ||
selectors.insert(nextFirstSelector, item); | ||
return true; | ||
@@ -56,3 +65,3 @@ } | ||
if (!item.next) { | ||
selector.insert(nextFirstSelector); | ||
selectors.insert(nextFirstSelector); | ||
return true; | ||
@@ -59,0 +68,0 @@ } |
@@ -27,5 +27,9 @@ var List = require('../../utils/list.js'); | ||
function inList(selector) { | ||
return selector.compareMarker in this; | ||
} | ||
module.exports = function restructRuleset(node, item, list) { | ||
var avoidRulesMerge = this.stylesheet.avoidRulesMerge; | ||
var selector = node.selector.selectors; | ||
var selectors = node.selector.selectors; | ||
var block = node.block; | ||
@@ -35,7 +39,8 @@ var skippedCompareMarkers = Object.create(null); | ||
list.prevUntil(item.prev, function(prev, prevItem) { | ||
// skip non-ruleset node if safe | ||
if (prev.type !== 'Ruleset') { | ||
return true; | ||
return utils.unsafeToSkipNode.call(selectors, prev); | ||
} | ||
var prevSelector = prev.selector.selectors; | ||
var prevSelectors = prev.selector.selectors; | ||
var prevBlock = prev.block; | ||
@@ -48,13 +53,8 @@ | ||
// try prev ruleset if simpleselectors has no equal specifity and element selector | ||
var prevSelectorCursor = prevSelector.head; | ||
while (prevSelectorCursor) { | ||
if (prevSelectorCursor.data.compareMarker in skippedCompareMarkers) { | ||
return true; | ||
} | ||
prevSelectorCursor = prevSelectorCursor.next; | ||
if (prevSelectors.some(inList, skippedCompareMarkers)) { | ||
return true; | ||
} | ||
// try to join by selectors | ||
if (utils.isEqualLists(prevSelector, selector)) { | ||
if (utils.isEqualLists(prevSelectors, selectors)) { | ||
prevBlock.declarations.appendList(block.declarations); | ||
@@ -73,3 +73,3 @@ list.remove(item); | ||
// equal blocks | ||
utils.addToSelector(selector, prevSelector); | ||
utils.addSelectors(selectors, prevSelectors); | ||
list.remove(prevItem); | ||
@@ -82,17 +82,17 @@ return true; | ||
// prevBlock is subset block | ||
var selectorLength = calcSelectorLength(selector); | ||
var selectorLength = calcSelectorLength(selectors); | ||
var blockLength = calcDeclarationsLength(diff.eq); // declarations length | ||
if (selectorLength < blockLength) { | ||
utils.addToSelector(prevSelector, selector); | ||
node.block.declarations = new List(diff.ne1); | ||
utils.addSelectors(prevSelectors, selectors); | ||
block.declarations = new List(diff.ne1); | ||
} | ||
} else if (!diff.ne1.length && diff.ne2.length) { | ||
// node is subset of prevBlock | ||
var selectorLength = calcSelectorLength(prevSelector); | ||
var selectorLength = calcSelectorLength(prevSelectors); | ||
var blockLength = calcDeclarationsLength(diff.eq); // declarations length | ||
if (selectorLength < blockLength) { | ||
utils.addToSelector(selector, prevSelector); | ||
prev.block.declarations = new List(diff.ne2); | ||
utils.addSelectors(selectors, prevSelectors); | ||
prevBlock.declarations = new List(diff.ne2); | ||
} | ||
@@ -105,3 +105,3 @@ } else { | ||
info: {}, | ||
selectors: utils.addToSelector(prevSelector.copy(), selector) | ||
selectors: utils.addSelectors(prevSelectors.copy(), selectors) | ||
}; | ||
@@ -126,4 +126,4 @@ var newBlockLength = calcSelectorLength(newSelector.selectors) + 2; // selectors length + curly braces length | ||
node.block.declarations = new List(diff.ne1); | ||
prev.block.declarations = new List(diff.ne2.concat(diff.ne2overrided)); | ||
block.declarations = new List(diff.ne1); | ||
prevBlock.declarations = new List(diff.ne2.concat(diff.ne2overrided)); | ||
list.insert(list.createItem(newRuleset), prevItem); | ||
@@ -136,3 +136,3 @@ return true; | ||
prevSelector.each(function(data) { | ||
prevSelectors.each(function(data) { | ||
skippedCompareMarkers[data.compareMarker] = true; | ||
@@ -139,0 +139,0 @@ }); |
var internalWalkRules = require('../ast/walk.js').rules; | ||
var internalWalkRulesRight = require('../ast/walk.js').rulesRight; | ||
var translate = require('../ast/translate.js'); | ||
var prepare = require('./prepare/index.js'); | ||
@@ -13,19 +12,3 @@ var initialMergeRuleset = require('./1-initialMergeRuleset.js'); | ||
function Index() { | ||
this.seed = 0; | ||
this.map = Object.create(null); | ||
} | ||
Index.prototype.resolve = function(str) { | ||
var index = this.map[str]; | ||
if (!index) { | ||
index = ++this.seed; | ||
this.map[str] = index; | ||
} | ||
return index; | ||
}; | ||
module.exports = function(ast, debug) { | ||
module.exports = function(ast, usageData, debug) { | ||
function walkRulesets(name, fn) { | ||
@@ -61,31 +44,15 @@ internalWalkRules(ast, function(node, item, list) { | ||
var declarationMarker = (function() { | ||
var names = new Index(); | ||
var values = new Index(); | ||
return function markDeclaration(node) { | ||
// node.id = translate(node); | ||
var property = node.property.name; | ||
var value = translate(node.value); | ||
node.id = names.resolve(property) + (values.resolve(value) << 12); | ||
node.length = property.length + 1 + value.length; | ||
return node; | ||
}; | ||
})(); | ||
// prepare ast for restructing | ||
internalWalkRules(ast, function(node) { | ||
prepare(node, declarationMarker); | ||
}); | ||
var indexer = prepare(ast, usageData); | ||
debug('prepare', ast); | ||
// todo: remove initial merge | ||
walkRulesetsRight('initialMergeRuleset', initialMergeRuleset); | ||
// NOTE: direction should be left to right, since rulesets merge to left | ||
// ruleset. When direction right to left unmerged rulesets may prevent lookup | ||
// TODO: remove initial merge | ||
walkRulesets('initialMergeRuleset', initialMergeRuleset); | ||
walkAtrules('mergeAtrule', mergeAtrule); | ||
walkRulesetsRight('disjoinRuleset', disjoinRuleset); | ||
restructShorthand(ast, declarationMarker); | ||
restructShorthand(ast, indexer); | ||
debug('restructShorthand', ast); | ||
@@ -92,0 +59,0 @@ |
@@ -0,10 +1,12 @@ | ||
var internalWalkRules = require('../../ast/walk.js').rules; | ||
var resolveKeyword = require('../../ast/names.js').keyword; | ||
var translate = require('../../ast/translate.js'); | ||
var createDeclarationIndexer = require('./createDeclarationIndexer.js'); | ||
var processSelector = require('./processSelector.js'); | ||
module.exports = function walk(node, markDeclaration) { | ||
function walk(node, markDeclaration, usageData) { | ||
switch (node.type) { | ||
case 'Ruleset': | ||
node.block.declarations.each(markDeclaration); | ||
processSelector(node); | ||
processSelector(node, usageData); | ||
break; | ||
@@ -31,1 +33,13 @@ | ||
}; | ||
module.exports = function prepare(ast, usageData) { | ||
var markDeclaration = createDeclarationIndexer(); | ||
internalWalkRules(ast, function(node) { | ||
walk(node, markDeclaration, usageData); | ||
}); | ||
return { | ||
declaration: markDeclaration | ||
}; | ||
}; |
@@ -21,3 +21,3 @@ var translate = require('../../ast/translate.js'); | ||
module.exports = function freeze(node) { | ||
module.exports = function freeze(node, usageData) { | ||
var pseudos = Object.create(null); | ||
@@ -27,19 +27,19 @@ var hasPseudo = false; | ||
node.selector.selectors.each(function(simpleSelector) { | ||
var list = simpleSelector.sequence; | ||
var last = list.tail; | ||
var tagName = '*'; | ||
var scope = 0; | ||
while (last && last.prev && last.prev.data.type !== 'Combinator') { | ||
last = last.prev; | ||
} | ||
simpleSelector.sequence.some(function(node) { | ||
switch (node.type) { | ||
case 'Class': | ||
if (usageData && usageData.scopes) { | ||
var classScope = usageData.scopes[node.name] || 0; | ||
if (last && last.data.type === 'Identifier') { | ||
tagName = last.data.name; | ||
} | ||
if (scope !== 0 && classScope !== scope) { | ||
throw new Error('Selector can\'t has classes from different scopes: ' + translate(simpleSelector)); | ||
} | ||
simpleSelector.compareMarker = specificity(simpleSelector) + ',' + tagName; | ||
simpleSelector.id = translate(simpleSelector); | ||
scope = classScope; | ||
} | ||
break; | ||
simpleSelector.sequence.each(function(node) { | ||
switch (node.type) { | ||
case 'PseudoClass': | ||
@@ -63,4 +63,20 @@ if (!nonFreezePseudoClasses.hasOwnProperty(node.name)) { | ||
break; | ||
case 'Negation': | ||
pseudos.not = true; | ||
hasPseudo = true; | ||
break; | ||
case 'Identifier': | ||
tagName = node.name; | ||
break; | ||
case 'Combinator': | ||
tagName = '*'; | ||
break; | ||
} | ||
}); | ||
simpleSelector.id = translate(simpleSelector); | ||
simpleSelector.compareMarker = specificity(simpleSelector) + ',' + tagName + (scope ? ',' + scope : ''); | ||
}); | ||
@@ -67,0 +83,0 @@ |
@@ -75,3 +75,3 @@ var hasOwnProperty = Object.prototype.hasOwnProperty; | ||
function addToSelector(dest, source) { | ||
function addSelectors(dest, source) { | ||
source.each(function(sourceData) { | ||
@@ -101,2 +101,36 @@ var newStr = sourceData.id; | ||
// check if simpleselectors has no equal specificity and element selector | ||
function hasSimilarSelectors(selectors1, selectors2) { | ||
return selectors1.some(function(a) { | ||
return selectors2.some(function(b) { | ||
return a.compareMarker === b.compareMarker; | ||
}); | ||
}); | ||
} | ||
// test node can't to be skipped | ||
function unsafeToSkipNode(node) { | ||
switch (node.type) { | ||
case 'Ruleset': | ||
// unsafe skip ruleset with selector similarities | ||
return hasSimilarSelectors(node.selector.selectors, this); | ||
case 'Atrule': | ||
// can skip at-rules with blocks | ||
if (node.block) { | ||
// non-stylesheet blocks are safe to skip since have no selectors | ||
if (node.block.type !== 'StyleSheet') { | ||
return false; | ||
} | ||
// unsafe skip at-rule if block contains something unsafe to skip | ||
return node.block.rules.some(unsafeToSkipNode, this); | ||
} | ||
break; | ||
} | ||
// unsafe by default | ||
return true; | ||
} | ||
module.exports = { | ||
@@ -106,3 +140,5 @@ isEqualLists: isEqualLists, | ||
compareDeclarations: compareDeclarations, | ||
addToSelector: addToSelector | ||
addSelectors: addSelectors, | ||
hasSimilarSelectors: hasSimilarSelectors, | ||
unsafeToSkipNode: unsafeToSkipNode | ||
}; |
@@ -57,19 +57,24 @@ var parse = require('./parser'); | ||
function buildCompressOptions(options) { | ||
function copy(obj) { | ||
var result = {}; | ||
for (var key in options) { | ||
result[key] = options[key]; | ||
for (var key in obj) { | ||
result[key] = obj[key]; | ||
} | ||
result.outputAst = 'internal'; | ||
return result; | ||
} | ||
if (typeof result.logger !== 'function' && options.debug) { | ||
result.logger = createDefaultLogger(options.debug); | ||
function buildCompressOptions(options) { | ||
options = copy(options); | ||
options.outputAst = 'internal'; | ||
if (typeof options.logger !== 'function' && options.debug) { | ||
options.logger = createDefaultLogger(options.debug); | ||
} | ||
return result; | ||
return options; | ||
} | ||
var minify = function(source, options) { | ||
function minify(context, source, options) { | ||
options = options || {}; | ||
@@ -82,3 +87,3 @@ | ||
var ast = debugOutput('parsing', options, new Date(), | ||
parse(source, 'stylesheet', { | ||
parse(source, context, { | ||
filename: filename, | ||
@@ -110,4 +115,12 @@ positions: Boolean(options.sourceMap), | ||
return result; | ||
} | ||
function minifyStylesheet(source, options) { | ||
return minify('stylesheet', source, options); | ||
}; | ||
function minifyBlock(source, options) { | ||
return minify('declarations', source, options); | ||
} | ||
module.exports = { | ||
@@ -117,3 +130,4 @@ version: require('../package.json').version, | ||
// main method | ||
minify: minify, | ||
minify: minifyStylesheet, | ||
minifyBlock: minifyBlock, | ||
@@ -120,0 +134,0 @@ // utils |
@@ -32,17 +32,13 @@ 'use strict'; | ||
'atkeyword': getAtkeyword, | ||
'atruleb': getAtrule, | ||
'atruler': getAtrule, | ||
'atrules': getAtrule, | ||
'attrib': getAttrib, | ||
'attrselector': getAttrselector, | ||
'block': getBlock, | ||
'atrule': getAtrule, | ||
'attribute': getAttribute, | ||
'block': getBlockWithBrackets, | ||
'braces': getBraces, | ||
'clazz': getClass, | ||
'class': getClass, | ||
'combinator': getCombinator, | ||
'comment': getComment, | ||
'declaration': getDeclaration, | ||
'declarations': getBlock, | ||
'dimension': getDimension, | ||
'filter': getDeclaration, | ||
'functionExpression': getOldIEExpression, | ||
'funktion': getFunction, | ||
'function': getFunction, | ||
'ident': getIdentifier, | ||
@@ -57,4 +53,4 @@ 'important': getImportant, | ||
'property': getProperty, | ||
'pseudoc': getPseudoc, | ||
'pseudoe': getPseudoe, | ||
'pseudoClass': getPseudoClass, | ||
'pseudoElement': getPseudoElement, | ||
'ruleset': getRuleset, | ||
@@ -70,3 +66,17 @@ 'selector': getSelector, | ||
'value': getValue, | ||
'vhash': getVhash | ||
'vhash': getVhash, | ||
// TODO: remove in 2.0 | ||
// for backward capability | ||
'atruleb': getAtrule, | ||
'atruler': getAtrule, | ||
'atrules': getAtrule, | ||
'attrib': getAttribute, | ||
'attrselector': getAttrselector, | ||
'clazz': getClass, | ||
'filter': getDeclaration, | ||
'functionExpression': getOldIEExpression, | ||
'funktion': getFunction, | ||
'pseudoc': getPseudoClass, | ||
'pseudoe': getPseudoElement | ||
}; | ||
@@ -235,3 +245,3 @@ | ||
node[1] = NodeType.AtrulebType; | ||
node.push(getBlock()); | ||
node.push(getBlockWithBrackets()); | ||
} else { | ||
@@ -287,3 +297,3 @@ node[1] = NodeType.AtrulerType; | ||
getSelector(), | ||
getBlock() | ||
getBlockWithBrackets() | ||
]; | ||
@@ -359,3 +369,3 @@ } | ||
case TokenType.LeftSquareBracket: | ||
node.push(getAttrib()); | ||
node.push(getAttribute()); | ||
break; | ||
@@ -390,7 +400,16 @@ | ||
function getBlock() { | ||
var node = [getInfo(pos), NodeType.BlockType]; | ||
function getBlockWithBrackets() { | ||
var info = getInfo(pos); | ||
var node; | ||
eat(TokenType.LeftCurlyBracket); | ||
node = getBlock(info); | ||
eat(TokenType.RightCurlyBracket); | ||
return node; | ||
} | ||
function getBlock(info) { | ||
var node = [info || getInfo(pos), NodeType.BlockType]; | ||
scan: | ||
@@ -422,4 +441,2 @@ while (pos < tokens.length) { | ||
eat(TokenType.RightCurlyBracket); | ||
return node; | ||
@@ -560,2 +577,6 @@ } | ||
case TokenType.LowLine: | ||
case TokenType.Identifier: | ||
break; | ||
case TokenType.FullStop: | ||
@@ -591,9 +612,3 @@ case TokenType.DecimalNumber: | ||
parseError('Unexpected input'); | ||
break; | ||
case TokenType.HyphenMinus: | ||
case TokenType.LowLine: | ||
case TokenType.Identifier: | ||
break; | ||
default: | ||
@@ -615,3 +630,3 @@ parseError('Unexpected input'); | ||
// '[' S* attrib_name attrib_match [ IDENT | STRING ] S* attrib_flags? ']' | ||
function getAttrib() { | ||
function getAttribute() { | ||
var node = [getInfo(pos), NodeType.AttribType]; | ||
@@ -1492,3 +1507,3 @@ | ||
if (next.type === TokenType.Colon) { | ||
return getPseudoe(); | ||
return getPseudoElement(); | ||
} | ||
@@ -1501,7 +1516,7 @@ | ||
return getPseudoc(); | ||
return getPseudoClass(); | ||
} | ||
// :: ident | ||
function getPseudoe() { | ||
function getPseudoElement() { | ||
eat(TokenType.Colon); | ||
@@ -1514,3 +1529,3 @@ eat(TokenType.Colon); | ||
// : ( ident | function ) | ||
function getPseudoc() { | ||
function getPseudoClass() { | ||
var startPos = pos; | ||
@@ -1599,3 +1614,3 @@ var node = eat(TokenType.Colon) && getIdentifier(); | ||
module.exports = function parse(source, rule, options) { | ||
module.exports = function parse(source, context, options) { | ||
var ast; | ||
@@ -1620,9 +1635,9 @@ | ||
filename = options.filename || '<unknown>'; | ||
rule = rule || 'stylesheet'; | ||
context = context || 'stylesheet'; | ||
pos = 0; | ||
tokens = tokenize(source, blockMode.hasOwnProperty(rule), options.line, options.column); | ||
tokens = tokenize(source, blockMode.hasOwnProperty(context), options.line, options.column); | ||
if (tokens.length) { | ||
ast = rules[rule](); | ||
ast = rules[context](); | ||
} | ||
@@ -1632,4 +1647,4 @@ | ||
if (!ast && rule === 'stylesheet') { | ||
ast = [{}, rule]; | ||
if (!ast && context === 'stylesheet') { | ||
ast = [{}, context]; | ||
} | ||
@@ -1636,0 +1651,0 @@ |
{ | ||
"name": "csso", | ||
"version": "1.7.1", | ||
"version": "1.8.0", | ||
"description": "CSSO (CSS Optimizer) is a CSS minifier with structural optimisations", | ||
@@ -37,2 +37,3 @@ "keywords": [ | ||
"rules": { | ||
"no-duplicate-case": 2, | ||
"no-undef": 2, | ||
@@ -43,7 +44,9 @@ "no-unused-vars": [2, {"vars": "all", "args": "after-used"}] | ||
"scripts": { | ||
"test": "jscs lib && eslint lib test && mocha --reporter dot", | ||
"test": "mocha --reporter dot", | ||
"codestyle": "jscs lib && eslint lib test", | ||
"codestyle-and-test": "npm run codestyle && npm test", | ||
"hydrogen": "node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --trace_hydrogen_file=code.cfg --print-opt-code bin/csso --stat -o /dev/null", | ||
"coverage": "istanbul cover _mocha -- -R dot", | ||
"coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | coveralls", | ||
"travis": "npm run test && npm run coveralls", | ||
"travis": "npm run codestyle-and-test && npm run coveralls", | ||
"browserify": "browserify --standalone csso lib/index.js | uglifyjs --compress --mangle -o dist/csso-browser.js", | ||
@@ -50,0 +53,0 @@ "gh-pages": "git clone -b gh-pages https://github.com/css/csso.git .gh-pages && npm run browserify && cp dist/csso-browser.js .gh-pages/ && cd .gh-pages && git commit -am \"update\" && git push && cd .. && rm -rf .gh-pages", |
113
README.md
@@ -9,4 +9,4 @@ [![NPM version](https://img.shields.io/npm/v/csso.svg)](https://www.npmjs.com/package/csso) | ||
[![Originated by Yandex](https://github.com/css/csso/blob/master/docs/yandex.png)](https://www.yandex.com/) | ||
[![Sponsored by Avito](https://github.com/css/csso/blob/master/docs/avito.png)](https://www.avito.ru/) | ||
[![Originated by Yandex](https://cdn.rawgit.com/css/csso/8d1b89211ac425909f735e7d5df87ee16c2feec6/docs/yandex.svg)](https://www.yandex.com/) | ||
[![Sponsored by Avito](https://cdn.rawgit.com/css/csso/8d1b89211ac425909f735e7d5df87ee16c2feec6/docs/avito.svg)](https://www.avito.ru/) | ||
@@ -38,7 +38,8 @@ ## Usage | ||
-i, --input <filename> Input file | ||
--input-map <source> Input source map. Possible values: none, auto (default) or <filename> | ||
-m, --map <destination> Generate source map. Possible values: none (default), inline, file or <filename> | ||
--input-map <source> Input source map: none, auto (default) or <filename> | ||
-m, --map <destination> Generate source map: none (default), inline, file or <filename> | ||
-o, --output <filename> Output file (result outputs to stdout if not set) | ||
--restructure-off Turns structure minimization off | ||
--stat Output statistics in stderr | ||
-u, --usage <filenane> Usage data file | ||
-v, --version Output version | ||
@@ -74,4 +75,4 @@ ``` | ||
- `none` (default) – don't generate source map | ||
- `inline` – generate map add it into result content (via `/*# sourceMappingURL=application/json;base64,...base64 encoded map... */`) | ||
- `file` – generate map and write it into file with same name as output file, but with `.map` extension; in this case `--output` option is required | ||
- `inline` – add source map into result CSS (via `/*# sourceMappingURL=application/json;base64,... */`) | ||
- `file` – write source map into file with same name as output file, but with `.map` extension (in this case `--output` option is required) | ||
- any other values treat as filename for generated source map | ||
@@ -83,17 +84,101 @@ | ||
> csso my.css --map inline | ||
> csso my.css --map file --output my.min.css | ||
> csso my.css -o my.min.css -m maps/my.min.map | ||
> csso my.css --output my.min.css --map file | ||
> csso my.css --output my.min.css --map maps/my.min.map | ||
``` | ||
Input can has a source map. Use `--input-map` option to specify input source map if needed. Possible values for option: | ||
Use `--input-map` option to specify input source map if needed. Possible values for option: | ||
- `auto` (auto) - attempt to fetch input source map by follow steps: | ||
- try to fetch inline map from source | ||
- try to fetch map filename from source and read its content | ||
- (when `--input` is specified) check for file with same name as input but with `.map` extension exists and read its content | ||
- `auto` (default) - attempt to fetch input source map by follow steps: | ||
- try to fetch inline map from input | ||
- try to fetch source map filename from input and read its content | ||
- (when `--input` is specified) check file with same name as input file but with `.map` extension exists and read its content | ||
- `none` - don't use input source map; actually it's using to disable `auto`-fetching | ||
- any other values treat as filename for input source map | ||
> NOTE: Input source map is using only if source map is generating. | ||
Generally you shouldn't care about input source map since defaults behaviour (`auto`) covers most use cases. | ||
> NOTE: Input source map is using only if output source map is generating. | ||
### Usage data | ||
`CSSO` can use data about how `CSS` is using for better compression. File with this data (`JSON` format) can be set using `--usage` option. Usage data may contain follow sections: | ||
- `tags` – white list of tags | ||
- `ids` – white list of ids | ||
- `classes` – white list of classes | ||
- `scopes` – groups of classes which never used with classes from other groups on single element | ||
All sections are optional. Value of `tags`, `ids` and `classes` should be array of strings, value of `scopes` should be an array of arrays of strings. Other values are ignoring. | ||
#### Selector filtering | ||
`tags`, `ids` and `classes` are using on clean stage to filter selectors that contains something that not in list. Selectors are filtering only by those kind of simple selector which white list is specified. For example, if only `tags` list is specified then type selectors are checking, and if selector hasn't any type selector (or even any type selector) it isn't filter. | ||
> `ids` and `classes` comparison is case sensetive, `tags` – is not. | ||
Input CSS: | ||
```css | ||
* { color: green; } | ||
ul, ol, li { color: blue; } | ||
UL.foo, span.bar { color: red; } | ||
``` | ||
Usage data: | ||
```json | ||
{ | ||
"tags": ["ul", "LI"] | ||
} | ||
``` | ||
Result CSS: | ||
```css | ||
*{color:green}ul,li{color:blue}ul.foo{color:red} | ||
``` | ||
#### Scopes | ||
Scopes is designed for CSS scope isolation solutions such as [css-modules](https://github.com/css-modules/css-modules). Scopes are similar to namespaces and defines lists of class names that exclusively used on some markup. This information allows the optimizer to move rulesets more agressive. Since it assumes selectors from different scopes can't to be matched on the same element. That leads to better ruleset merging. | ||
Suppose we have a file: | ||
```css | ||
.module1-foo { color: red; } | ||
.module1-bar { font-size: 1.5em; background: yellow; } | ||
.module2-baz { color: red; } | ||
.module2-qux { font-size: 1.5em; background: yellow; width: 50px; } | ||
``` | ||
It can be assumed that first two rules never used with second two on the same markup. But trully speaking we cann't know that for sure without markup. The optimizer doesn't know it eather and will perform safe transformations only. The result will be the same as input but with no spaces and some semicolons: | ||
```css | ||
.module1-foo{color:red}.module1-bar{font-size:1.5em;background:#ff0}.module2-baz{color:red}.module2-qux{font-size:1.5em;background:#ff0;width:50px} | ||
``` | ||
But with usage data `CSSO` can get better output. If follow usage data is provided: | ||
```json | ||
{ | ||
"scopes": [ | ||
["module1-foo", "module1-bar"], | ||
["module2-bar", "module2-baz"] | ||
] | ||
} | ||
``` | ||
New result (29 bytes extra saving): | ||
```css | ||
.module1-foo,.module2-baz{color:red}.module1-bar,.module2-qux{font-size:1.5em;background:#ff0}.module2-qux{width:50px} | ||
``` | ||
If class name doesn't specified in `scopes` it's considered that it belongs to default "scope". `scopes` doesn't affect `classes`. If class name present in `scopes` but missed in `classes` (both sections specified) it will be filtered. | ||
Note that class name can't be specified in several scopes. Also selector can't has classes from different scopes. In both cases an exception throws. | ||
Currently the optimizer doesn't care about out-of-bounds selectors order changing safety (i.e. selectors that may be matched to elements with no class name of scope, e.g. `.scope div` or `.scope ~ :last-child`) since assumes scoped CSS modules doesn't relay on it's order. It may be fix in future if to be an issue. | ||
### API | ||
@@ -100,0 +185,0 @@ |
Sorry, the diff of this file is too big to display
325303
56
6508
302