diff options
Diffstat (limited to 'node_modules/csso/lib/compressor/restructure')
13 files changed, 1459 insertions, 0 deletions
diff --git a/node_modules/csso/lib/compressor/restructure/1-initialMergeRuleset.js b/node_modules/csso/lib/compressor/restructure/1-initialMergeRuleset.js new file mode 100644 index 00000000..036a04b4 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/1-initialMergeRuleset.js @@ -0,0 +1,48 @@ +var utils = require('./utils.js'); +var walkRules = require('../../utils/walk.js').rules; + +function processRuleset(node, item, list) { + 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 utils.unsafeToSkipNode.call(selectors, prev); + } + + var prevSelectors = prev.selector.selectors; + var prevDeclarations = prev.block.declarations; + + // 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 declarations + if (utils.isEqualDeclarations(declarations, prevDeclarations)) { + utils.addSelectors(prevSelectors, selectors); + list.remove(item); + return true; + } + } + + // go to prev ruleset if has no selector similarities + return utils.hasSimilarSelectors(selectors, prevSelectors); + }); +}; + +// 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 +module.exports = function initialMergeRuleset(ast) { + walkRules(ast, function(node, item, list) { + if (node.type === 'Ruleset') { + processRuleset(node, item, list); + } + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/2-mergeAtrule.js b/node_modules/csso/lib/compressor/restructure/2-mergeAtrule.js new file mode 100644 index 00000000..d07318f7 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/2-mergeAtrule.js @@ -0,0 +1,35 @@ +var walkRulesRight = require('../../utils/walk.js').rulesRight; + +function isMediaRule(node) { + return node.type === 'Atrule' && node.name === 'media'; +} + +function processAtrule(node, item, list) { + if (!isMediaRule(node)) { + return; + } + + var prev = item.prev && item.prev.data; + + if (!prev || !isMediaRule(prev)) { + return; + } + + // merge @media with same query + if (node.expression.id === prev.expression.id) { + prev.block.rules.appendList(node.block.rules); + prev.info = { + primary: prev.info, + merged: node.info + }; + list.remove(item); + } +}; + +module.exports = function rejoinAtrule(ast) { + walkRulesRight(ast, function(node, item, list) { + if (node.type === 'Atrule') { + processAtrule(node, item, list); + } + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/3-disjoinRuleset.js b/node_modules/csso/lib/compressor/restructure/3-disjoinRuleset.js new file mode 100644 index 00000000..6df4f807 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/3-disjoinRuleset.js @@ -0,0 +1,42 @@ +var List = require('../../utils/list.js'); +var walkRulesRight = require('../../utils/walk.js').rulesRight; + +function processRuleset(node, item, list) { + var selectors = node.selector.selectors; + + // generate new rule sets: + // .a, .b { color: red; } + // -> + // .a { color: red; } + // .b { color: red; } + + // while there are more than 1 simple selector split for rulesets + while (selectors.head !== selectors.tail) { + var newSelectors = new List(); + newSelectors.insert(selectors.remove(selectors.head)); + + list.insert(list.createItem({ + type: 'Ruleset', + info: node.info, + pseudoSignature: node.pseudoSignature, + selector: { + type: 'Selector', + info: node.selector.info, + selectors: newSelectors + }, + block: { + type: 'Block', + info: node.block.info, + declarations: node.block.declarations.copy() + } + }), item); + } +}; + +module.exports = function disjoinRuleset(ast) { + walkRulesRight(ast, function(node, item, list) { + if (node.type === 'Ruleset') { + processRuleset(node, item, list); + } + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/4-restructShorthand.js b/node_modules/csso/lib/compressor/restructure/4-restructShorthand.js new file mode 100644 index 00000000..aa95e3cc --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/4-restructShorthand.js @@ -0,0 +1,430 @@ +var List = require('../../utils/list.js'); +var translate = require('../../utils/translate.js'); +var walkRulesRight = require('../../utils/walk.js').rulesRight; + +var REPLACE = 1; +var REMOVE = 2; +var TOP = 0; +var RIGHT = 1; +var BOTTOM = 2; +var LEFT = 3; +var SIDES = ['top', 'right', 'bottom', 'left']; +var SIDE = { + 'margin-top': 'top', + 'margin-right': 'right', + 'margin-bottom': 'bottom', + 'margin-left': 'left', + + 'padding-top': 'top', + 'padding-right': 'right', + 'padding-bottom': 'bottom', + 'padding-left': 'left', + + 'border-top-color': 'top', + 'border-right-color': 'right', + 'border-bottom-color': 'bottom', + 'border-left-color': 'left', + 'border-top-width': 'top', + 'border-right-width': 'right', + 'border-bottom-width': 'bottom', + 'border-left-width': 'left', + 'border-top-style': 'top', + 'border-right-style': 'right', + 'border-bottom-style': 'bottom', + 'border-left-style': 'left' +}; +var MAIN_PROPERTY = { + 'margin': 'margin', + 'margin-top': 'margin', + 'margin-right': 'margin', + 'margin-bottom': 'margin', + 'margin-left': 'margin', + + 'padding': 'padding', + 'padding-top': 'padding', + 'padding-right': 'padding', + 'padding-bottom': 'padding', + 'padding-left': 'padding', + + 'border-color': 'border-color', + 'border-top-color': 'border-color', + 'border-right-color': 'border-color', + 'border-bottom-color': 'border-color', + 'border-left-color': 'border-color', + 'border-width': 'border-width', + 'border-top-width': 'border-width', + 'border-right-width': 'border-width', + 'border-bottom-width': 'border-width', + 'border-left-width': 'border-width', + 'border-style': 'border-style', + 'border-top-style': 'border-style', + 'border-right-style': 'border-style', + 'border-bottom-style': 'border-style', + 'border-left-style': 'border-style' +}; + +function TRBL(name) { + this.name = name; + this.info = null; + this.iehack = undefined; + this.sides = { + 'top': null, + 'right': null, + 'bottom': null, + 'left': null + }; +} + +TRBL.prototype.getValueSequence = function(value, count) { + var values = []; + var iehack = ''; + var hasBadValues = value.sequence.some(function(child) { + var special = false; + + switch (child.type) { + case 'Identifier': + switch (child.name) { + case '\\0': + case '\\9': + iehack = child.name; + return; + + case 'inherit': + case 'initial': + case 'unset': + case 'revert': + special = child.name; + break; + } + break; + + case 'Dimension': + switch (child.unit) { + // is not supported until IE11 + case 'rem': + + // v* units is too buggy across browsers and better + // don't merge values with those units + case 'vw': + case 'vh': + case 'vmin': + case 'vmax': + case 'vm': // IE9 supporting "vm" instead of "vmin". + special = child.unit; + break; + } + break; + + case 'Hash': // color + case 'Number': + case 'Percentage': + break; + + case 'Function': + special = child.name; + break; + + case 'Space': + return false; // ignore space + + default: + return true; // bad value + } + + values.push({ + node: child, + special: special, + important: value.important + }); + }); + + if (hasBadValues || values.length > count) { + return false; + } + + if (typeof this.iehack === 'string' && this.iehack !== iehack) { + return false; + } + + this.iehack = iehack; // move outside + + return values; +}; + +TRBL.prototype.canOverride = function(side, value) { + var currentValue = this.sides[side]; + + return !currentValue || (value.important && !currentValue.important); +}; + +TRBL.prototype.add = function(name, value, info) { + function attemptToAdd() { + var sides = this.sides; + var side = SIDE[name]; + + if (side) { + if (side in sides === false) { + return false; + } + + var values = this.getValueSequence(value, 1); + + if (!values || !values.length) { + return false; + } + + // can mix only if specials are equal + for (var key in sides) { + if (sides[key] !== null && sides[key].special !== values[0].special) { + return false; + } + } + + if (!this.canOverride(side, values[0])) { + return true; + } + + sides[side] = values[0]; + return true; + } else if (name === this.name) { + var values = this.getValueSequence(value, 4); + + if (!values || !values.length) { + return false; + } + + switch (values.length) { + case 1: + values[RIGHT] = values[TOP]; + values[BOTTOM] = values[TOP]; + values[LEFT] = values[TOP]; + break; + + case 2: + values[BOTTOM] = values[TOP]; + values[LEFT] = values[RIGHT]; + break; + + case 3: + values[LEFT] = values[RIGHT]; + break; + } + + // can mix only if specials are equal + for (var i = 0; i < 4; i++) { + for (var key in sides) { + if (sides[key] !== null && sides[key].special !== values[i].special) { + return false; + } + } + } + + for (var i = 0; i < 4; i++) { + if (this.canOverride(SIDES[i], values[i])) { + sides[SIDES[i]] = values[i]; + } + } + + return true; + } + } + + if (!attemptToAdd.call(this)) { + return false; + } + + if (this.info) { + this.info = { + primary: this.info, + merged: info + }; + } else { + this.info = info; + } + + return true; +}; + +TRBL.prototype.isOkToMinimize = function() { + var top = this.sides.top; + var right = this.sides.right; + var bottom = this.sides.bottom; + var left = this.sides.left; + + if (top && right && bottom && left) { + var important = + top.important + + right.important + + bottom.important + + left.important; + + return important === 0 || important === 4; + } + + return false; +}; + +TRBL.prototype.getValue = function() { + var result = []; + var sides = this.sides; + var values = [ + sides.top, + sides.right, + sides.bottom, + sides.left + ]; + var stringValues = [ + translate(sides.top.node), + translate(sides.right.node), + translate(sides.bottom.node), + translate(sides.left.node) + ]; + + if (stringValues[LEFT] === stringValues[RIGHT]) { + values.pop(); + if (stringValues[BOTTOM] === stringValues[TOP]) { + values.pop(); + if (stringValues[RIGHT] === stringValues[TOP]) { + values.pop(); + } + } + } + + for (var i = 0; i < values.length; i++) { + if (i) { + result.push({ type: 'Space' }); + } + + result.push(values[i].node); + } + + if (this.iehack) { + result.push({ type: 'Space' }, { + type: 'Identifier', + info: {}, + name: this.iehack + }); + } + + return { + type: 'Value', + info: {}, + important: sides.top.important, + sequence: new List(result) + }; +}; + +TRBL.prototype.getProperty = function() { + return { + type: 'Property', + info: {}, + name: this.name + }; +}; + +function processRuleset(ruleset, shorts, shortDeclarations, lastShortSelector) { + var declarations = ruleset.block.declarations; + var selector = ruleset.selector.selectors.first().id; + + ruleset.block.declarations.eachRight(function(declaration, item) { + var property = declaration.property.name; + + if (!MAIN_PROPERTY.hasOwnProperty(property)) { + return; + } + + var key = MAIN_PROPERTY[property]; + var shorthand; + var operation; + + if (!lastShortSelector || selector === lastShortSelector) { + if (key in shorts) { + operation = REMOVE; + shorthand = shorts[key]; + } + } + + if (!shorthand || !shorthand.add(property, declaration.value, declaration.info)) { + operation = REPLACE; + shorthand = new TRBL(key); + + // if can't parse value ignore it and break shorthand sequence + if (!shorthand.add(property, declaration.value, declaration.info)) { + lastShortSelector = null; + return; + } + } + + shorts[key] = shorthand; + shortDeclarations.push({ + operation: operation, + block: declarations, + item: item, + shorthand: shorthand + }); + + lastShortSelector = selector; + }); + + return lastShortSelector; +}; + +function processShorthands(shortDeclarations, markDeclaration) { + shortDeclarations.forEach(function(item) { + var shorthand = item.shorthand; + + if (!shorthand.isOkToMinimize()) { + return; + } + + if (item.operation === REPLACE) { + item.item.data = markDeclaration({ + type: 'Declaration', + info: shorthand.info, + property: shorthand.getProperty(), + value: shorthand.getValue(), + id: 0, + length: 0, + fingerprint: null + }); + } else { + item.block.remove(item.item); + } + }); +}; + +module.exports = function restructBlock(ast, indexer) { + var stylesheetMap = {}; + var shortDeclarations = []; + + walkRulesRight(ast, function(node) { + if (node.type !== 'Ruleset') { + return; + } + + var stylesheet = this.stylesheet; + var rulesetId = (node.pseudoSignature || '') + '|' + node.selector.selectors.first().id; + var rulesetMap; + var shorts; + + if (!stylesheetMap.hasOwnProperty(stylesheet.id)) { + rulesetMap = { + lastShortSelector: null + }; + stylesheetMap[stylesheet.id] = rulesetMap; + } else { + rulesetMap = stylesheetMap[stylesheet.id]; + } + + if (rulesetMap.hasOwnProperty(rulesetId)) { + shorts = rulesetMap[rulesetId]; + } else { + shorts = {}; + rulesetMap[rulesetId] = shorts; + } + + rulesetMap.lastShortSelector = processRuleset.call(this, node, shorts, shortDeclarations, rulesetMap.lastShortSelector); + }); + + processShorthands(shortDeclarations, indexer.declaration); +}; diff --git a/node_modules/csso/lib/compressor/restructure/6-restructBlock.js b/node_modules/csso/lib/compressor/restructure/6-restructBlock.js new file mode 100644 index 00000000..4933eed4 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/6-restructBlock.js @@ -0,0 +1,261 @@ +var resolveProperty = require('../../utils/names.js').property; +var resolveKeyword = require('../../utils/names.js').keyword; +var walkRulesRight = require('../../utils/walk.js').rulesRight; +var translate = require('../../utils/translate.js'); +var dontRestructure = { + 'src': 1 // https://github.com/afelix/csso/issues/50 +}; + +var DONT_MIX_VALUE = { + // https://developer.mozilla.org/en-US/docs/Web/CSS/display#Browser_compatibility + 'display': /table|ruby|flex|-(flex)?box$|grid|contents|run-in/i, + // https://developer.mozilla.org/en/docs/Web/CSS/text-align + 'text-align': /^(start|end|match-parent|justify-all)$/i +}; + +var CURSOR_SAFE_VALUE = [ + 'auto', 'crosshair', 'default', 'move', 'text', 'wait', 'help', + 'n-resize', 'e-resize', 's-resize', 'w-resize', + 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize', + 'pointer', 'progress', 'not-allowed', 'no-drop', 'vertical-text', 'all-scroll', + 'col-resize', 'row-resize' +]; + +var NEEDLESS_TABLE = { + 'border-width': ['border'], + 'border-style': ['border'], + 'border-color': ['border'], + 'border-top': ['border'], + 'border-right': ['border'], + 'border-bottom': ['border'], + 'border-left': ['border'], + 'border-top-width': ['border-top', 'border-width', 'border'], + 'border-right-width': ['border-right', 'border-width', 'border'], + 'border-bottom-width': ['border-bottom', 'border-width', 'border'], + 'border-left-width': ['border-left', 'border-width', 'border'], + 'border-top-style': ['border-top', 'border-style', 'border'], + 'border-right-style': ['border-right', 'border-style', 'border'], + 'border-bottom-style': ['border-bottom', 'border-style', 'border'], + 'border-left-style': ['border-left', 'border-style', 'border'], + 'border-top-color': ['border-top', 'border-color', 'border'], + 'border-right-color': ['border-right', 'border-color', 'border'], + 'border-bottom-color': ['border-bottom', 'border-color', 'border'], + 'border-left-color': ['border-left', 'border-color', 'border'], + 'margin-top': ['margin'], + 'margin-right': ['margin'], + 'margin-bottom': ['margin'], + 'margin-left': ['margin'], + 'padding-top': ['padding'], + 'padding-right': ['padding'], + 'padding-bottom': ['padding'], + 'padding-left': ['padding'], + 'font-style': ['font'], + 'font-variant': ['font'], + 'font-weight': ['font'], + 'font-size': ['font'], + 'font-family': ['font'], + 'list-style-type': ['list-style'], + 'list-style-position': ['list-style'], + 'list-style-image': ['list-style'] +}; + +function getPropertyFingerprint(propertyName, declaration, fingerprints) { + var realName = resolveProperty(propertyName).name; + + if (realName === 'background' || + (realName === 'filter' && declaration.value.sequence.first().type === 'Progid')) { + return propertyName + ':' + translate(declaration.value); + } + + var declarationId = declaration.id; + var fingerprint = fingerprints[declarationId]; + + if (!fingerprint) { + var vendorId = ''; + var iehack = ''; + var special = {}; + + declaration.value.sequence.each(function walk(node) { + switch (node.type) { + case 'Argument': + case 'Value': + case 'Braces': + node.sequence.each(walk); + break; + + case 'Identifier': + var name = node.name; + + if (!vendorId) { + vendorId = resolveKeyword(name).vendor; + } + + if (/\\[09]/.test(name)) { + iehack = RegExp.lastMatch; + } + + if (realName === 'cursor') { + if (CURSOR_SAFE_VALUE.indexOf(name) === -1) { + special[name] = true; + } + } else if (DONT_MIX_VALUE.hasOwnProperty(realName)) { + if (DONT_MIX_VALUE[realName].test(name)) { + special[name] = true; + } + } + + break; + + case 'Function': + var name = node.name; + + if (!vendorId) { + vendorId = resolveKeyword(name).vendor; + } + + if (name === 'rect') { + // there are 2 forms of rect: + // rect(<top>, <right>, <bottom>, <left>) - standart + // rect(<top> <right> <bottom> <left>) – backwards compatible syntax + // only the same form values can be merged + if (node.arguments.size < 4) { + name = 'rect-backward'; + } + } + + special[name + '()'] = true; + + // check nested tokens too + node.arguments.each(walk); + + break; + + case 'Dimension': + var unit = node.unit; + + switch (unit) { + // is not supported until IE11 + case 'rem': + + // v* units is too buggy across browsers and better + // don't merge values with those units + case 'vw': + case 'vh': + case 'vmin': + case 'vmax': + case 'vm': // IE9 supporting "vm" instead of "vmin". + special[unit] = true; + break; + } + break; + } + }); + + fingerprint = '|' + Object.keys(special).sort() + '|' + iehack + vendorId; + + fingerprints[declarationId] = fingerprint; + } + + return propertyName + fingerprint; +} + +function needless(props, declaration, fingerprints) { + var property = resolveProperty(declaration.property.name); + + if (NEEDLESS_TABLE.hasOwnProperty(property.name)) { + var table = NEEDLESS_TABLE[property.name]; + + for (var i = 0; i < table.length; i++) { + var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints); + var prev = props[ppre]; + + if (prev && (!declaration.value.important || prev.item.data.value.important)) { + return prev; + } + } + } +} + +function processRuleset(ruleset, item, list, props, fingerprints) { + var declarations = ruleset.block.declarations; + + declarations.eachRight(function(declaration, declarationItem) { + var property = declaration.property.name; + var fingerprint = getPropertyFingerprint(property, declaration, fingerprints); + var prev = props[fingerprint]; + + if (prev && !dontRestructure.hasOwnProperty(property)) { + if (declaration.value.important && !prev.item.data.value.important) { + props[fingerprint] = { + block: declarations, + item: declarationItem + }; + + prev.block.remove(prev.item); + declaration.info = { + primary: declaration.info, + merged: prev.item.data.info + }; + } else { + declarations.remove(declarationItem); + prev.item.data.info = { + primary: prev.item.data.info, + merged: declaration.info + }; + } + } else { + var prev = needless(props, declaration, fingerprints); + + if (prev) { + declarations.remove(declarationItem); + prev.item.data.info = { + primary: prev.item.data.info, + merged: declaration.info + }; + } else { + declaration.fingerprint = fingerprint; + + props[fingerprint] = { + block: declarations, + item: declarationItem + }; + } + } + }); + + if (declarations.isEmpty()) { + list.remove(item); + } +}; + +module.exports = function restructBlock(ast) { + var stylesheetMap = {}; + var fingerprints = Object.create(null); + + walkRulesRight(ast, function(node, item, list) { + if (node.type !== 'Ruleset') { + return; + } + + var stylesheet = this.stylesheet; + var rulesetId = (node.pseudoSignature || '') + '|' + node.selector.selectors.first().id; + var rulesetMap; + var props; + + if (!stylesheetMap.hasOwnProperty(stylesheet.id)) { + rulesetMap = {}; + stylesheetMap[stylesheet.id] = rulesetMap; + } else { + rulesetMap = stylesheetMap[stylesheet.id]; + } + + if (rulesetMap.hasOwnProperty(rulesetId)) { + props = rulesetMap[rulesetId]; + } else { + props = {}; + rulesetMap[rulesetId] = props; + } + + processRuleset.call(this, node, item, list, props, fingerprints); + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/7-mergeRuleset.js b/node_modules/csso/lib/compressor/restructure/7-mergeRuleset.js new file mode 100644 index 00000000..0ae7edb6 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/7-mergeRuleset.js @@ -0,0 +1,87 @@ +var utils = require('./utils.js'); +var walkRules = require('../../utils/walk.js').rules; + +/* + 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 { ... } +*/ + +function processRuleset(node, item, list) { + 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 utils.unsafeToSkipNode.call(selectors, next); + } + + if (node.pseudoSignature !== next.pseudoSignature) { + return true; + } + + var nextFirstSelector = next.selector.selectors.head; + var nextDeclarations = next.block.declarations; + var nextCompareMarker = nextFirstSelector.data.compareMarker; + + // if next ruleset has same marked as one of skipped then stop joining + if (nextCompareMarker in skippedCompareMarkers) { + return true; + } + + // try to join by selectors + if (selectors.head === selectors.tail) { + if (selectors.first().id === nextFirstSelector.data.id) { + declarations.appendList(nextDeclarations); + list.remove(nextItem); + return; + } + } + + // try to join by properties + if (utils.isEqualDeclarations(declarations, nextDeclarations)) { + var nextStr = nextFirstSelector.data.id; + + selectors.some(function(data, item) { + var curStr = data.id; + + if (nextStr < curStr) { + selectors.insert(nextFirstSelector, item); + return true; + } + + if (!item.next) { + selectors.insert(nextFirstSelector); + return true; + } + }); + + list.remove(nextItem); + return; + } + + // go to next ruleset if current one can be skipped (has no equal specificity nor element selector) + if (nextCompareMarker === nodeCompareMarker) { + return true; + } + + skippedCompareMarkers[nextCompareMarker] = true; + }); +}; + +module.exports = function mergeRuleset(ast) { + walkRules(ast, function(node, item, list) { + if (node.type === 'Ruleset') { + processRuleset(node, item, list); + } + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/8-restructRuleset.js b/node_modules/csso/lib/compressor/restructure/8-restructRuleset.js new file mode 100644 index 00000000..9a9e545f --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/8-restructRuleset.js @@ -0,0 +1,157 @@ +var List = require('../../utils/list.js'); +var utils = require('./utils.js'); +var walkRulesRight = require('../../utils/walk.js').rulesRight; + +function calcSelectorLength(list) { + var length = 0; + + list.each(function(data) { + length += data.id.length + 1; + }); + + return length - 1; +} + +function calcDeclarationsLength(tokens) { + var length = 0; + + for (var i = 0; i < tokens.length; i++) { + length += tokens[i].length; + } + + return ( + length + // declarations + tokens.length - 1 // delimeters + ); +} + +function processRuleset(node, item, list) { + var avoidRulesMerge = this.stylesheet.avoidRulesMerge; + var selectors = node.selector.selectors; + var block = node.block; + var disallowDownMarkers = Object.create(null); + var allowMergeUp = true; + var allowMergeDown = true; + + list.prevUntil(item.prev, function(prev, prevItem) { + // skip non-ruleset node if safe + if (prev.type !== 'Ruleset') { + return utils.unsafeToSkipNode.call(selectors, prev); + } + + var prevSelectors = prev.selector.selectors; + var prevBlock = prev.block; + + if (node.pseudoSignature !== prev.pseudoSignature) { + return true; + } + + allowMergeDown = !prevSelectors.some(function(selector) { + return selector.compareMarker in disallowDownMarkers; + }); + + // try prev ruleset if simpleselectors has no equal specifity and element selector + if (!allowMergeDown && !allowMergeUp) { + return true; + } + + // try to join by selectors + if (allowMergeUp && utils.isEqualLists(prevSelectors, selectors)) { + prevBlock.declarations.appendList(block.declarations); + list.remove(item); + return true; + } + + // try to join by properties + var diff = utils.compareDeclarations(block.declarations, prevBlock.declarations); + + // console.log(diff.eq, diff.ne1, diff.ne2); + + if (diff.eq.length) { + if (!diff.ne1.length && !diff.ne2.length) { + // equal blocks + if (allowMergeDown) { + utils.addSelectors(selectors, prevSelectors); + list.remove(prevItem); + } + + return true; + } else if (!avoidRulesMerge) { /* probably we don't need to prevent those merges for @keyframes + TODO: need to be checked */ + + if (diff.ne1.length && !diff.ne2.length) { + // prevBlock is subset block + var selectorLength = calcSelectorLength(selectors); + var blockLength = calcDeclarationsLength(diff.eq); // declarations length + + if (allowMergeUp && selectorLength < blockLength) { + 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(prevSelectors); + var blockLength = calcDeclarationsLength(diff.eq); // declarations length + + if (allowMergeDown && selectorLength < blockLength) { + utils.addSelectors(selectors, prevSelectors); + prevBlock.declarations = new List(diff.ne2); + } + } else { + // diff.ne1.length && diff.ne2.length + // extract equal block + var newSelector = { + type: 'Selector', + info: {}, + selectors: utils.addSelectors(prevSelectors.copy(), selectors) + }; + var newBlockLength = calcSelectorLength(newSelector.selectors) + 2; // selectors length + curly braces length + var blockLength = calcDeclarationsLength(diff.eq); // declarations length + + // create new ruleset if declarations length greater than + // ruleset description overhead + if (allowMergeDown && blockLength >= newBlockLength) { + var newRuleset = { + type: 'Ruleset', + info: {}, + pseudoSignature: node.pseudoSignature, + selector: newSelector, + block: { + type: 'Block', + info: {}, + declarations: new List(diff.eq) + } + }; + + block.declarations = new List(diff.ne1); + prevBlock.declarations = new List(diff.ne2.concat(diff.ne2overrided)); + list.insert(list.createItem(newRuleset), prevItem); + return true; + } + } + } + } + + if (allowMergeUp) { + // TODO: disallow up merge only if any property interception only (i.e. diff.ne2overrided.length > 0); + // await property families to find property interception correctly + allowMergeUp = !prevSelectors.some(function(prevSelector) { + return selectors.some(function(selector) { + return selector.compareMarker === prevSelector.compareMarker; + }); + }); + } + + prevSelectors.each(function(data) { + disallowDownMarkers[data.compareMarker] = true; + }); + }); +}; + +module.exports = function restructRuleset(ast) { + walkRulesRight(ast, function(node, item, list) { + if (node.type === 'Ruleset') { + processRuleset.call(this, node, item, list); + } + }); +}; diff --git a/node_modules/csso/lib/compressor/restructure/index.js b/node_modules/csso/lib/compressor/restructure/index.js new file mode 100644 index 00000000..6a05974e --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/index.js @@ -0,0 +1,35 @@ +var prepare = require('./prepare/index.js'); +var initialMergeRuleset = require('./1-initialMergeRuleset.js'); +var mergeAtrule = require('./2-mergeAtrule.js'); +var disjoinRuleset = require('./3-disjoinRuleset.js'); +var restructShorthand = require('./4-restructShorthand.js'); +var restructBlock = require('./6-restructBlock.js'); +var mergeRuleset = require('./7-mergeRuleset.js'); +var restructRuleset = require('./8-restructRuleset.js'); + +module.exports = function(ast, usageData, debug) { + // prepare ast for restructing + var indexer = prepare(ast, usageData); + debug('prepare', ast); + + initialMergeRuleset(ast); + debug('initialMergeRuleset', ast); + + mergeAtrule(ast); + debug('mergeAtrule', ast); + + disjoinRuleset(ast); + debug('disjoinRuleset', ast); + + restructShorthand(ast, indexer); + debug('restructShorthand', ast); + + restructBlock(ast); + debug('restructBlock', ast); + + mergeRuleset(ast); + debug('mergeRuleset', ast); + + restructRuleset(ast); + debug('restructRuleset', ast); +}; diff --git a/node_modules/csso/lib/compressor/restructure/prepare/createDeclarationIndexer.js b/node_modules/csso/lib/compressor/restructure/prepare/createDeclarationIndexer.js new file mode 100644 index 00000000..c5235309 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/prepare/createDeclarationIndexer.js @@ -0,0 +1,32 @@ +var translate = require('../../../utils/translate.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 createDeclarationIndexer() { + var names = new Index(); + var values = new Index(); + + return function markDeclaration(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; + }; +}; diff --git a/node_modules/csso/lib/compressor/restructure/prepare/index.js b/node_modules/csso/lib/compressor/restructure/prepare/index.js new file mode 100644 index 00000000..075dc5f1 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/prepare/index.js @@ -0,0 +1,44 @@ +var resolveKeyword = require('../../../utils/names.js').keyword; +var walkRules = require('../../../utils/walk.js').rules; +var translate = require('../../../utils/translate.js'); +var createDeclarationIndexer = require('./createDeclarationIndexer.js'); +var processSelector = require('./processSelector.js'); + +function walk(node, markDeclaration, usageData) { + switch (node.type) { + case 'Ruleset': + node.block.declarations.each(markDeclaration); + processSelector(node, usageData); + break; + + case 'Atrule': + if (node.expression) { + node.expression.id = translate(node.expression); + } + + // compare keyframe selectors by its values + // NOTE: still no clarification about problems with keyframes selector grouping (issue #197) + if (resolveKeyword(node.name).name === 'keyframes') { + node.block.avoidRulesMerge = true; /* probably we don't need to prevent those merges for @keyframes + TODO: need to be checked */ + node.block.rules.each(function(ruleset) { + ruleset.selector.selectors.each(function(simpleselector) { + simpleselector.compareMarker = simpleselector.id; + }); + }); + } + break; + } +}; + +module.exports = function prepare(ast, usageData) { + var markDeclaration = createDeclarationIndexer(); + + walkRules(ast, function(node) { + walk(node, markDeclaration, usageData); + }); + + return { + declaration: markDeclaration + }; +}; diff --git a/node_modules/csso/lib/compressor/restructure/prepare/processSelector.js b/node_modules/csso/lib/compressor/restructure/prepare/processSelector.js new file mode 100644 index 00000000..56c46b56 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/prepare/processSelector.js @@ -0,0 +1,99 @@ +var translate = require('../../../utils/translate.js'); +var specificity = require('./specificity.js'); + +var nonFreezePseudoElements = { + 'first-letter': true, + 'first-line': true, + 'after': true, + 'before': true +}; +var nonFreezePseudoClasses = { + 'link': true, + 'visited': true, + 'hover': true, + 'active': true, + 'first-letter': true, + 'first-line': true, + 'after': true, + 'before': true +}; + +module.exports = function freeze(node, usageData) { + var pseudos = Object.create(null); + var hasPseudo = false; + + node.selector.selectors.each(function(simpleSelector) { + var tagName = '*'; + var scope = 0; + + simpleSelector.sequence.some(function(node) { + switch (node.type) { + case 'Class': + if (usageData && usageData.scopes) { + var classScope = usageData.scopes[node.name] || 0; + + if (scope !== 0 && classScope !== scope) { + throw new Error('Selector can\'t has classes from different scopes: ' + translate(simpleSelector)); + } + + scope = classScope; + } + break; + + case 'PseudoClass': + if (!nonFreezePseudoClasses.hasOwnProperty(node.name)) { + pseudos[node.name] = true; + hasPseudo = true; + } + break; + + case 'PseudoElement': + if (!nonFreezePseudoElements.hasOwnProperty(node.name)) { + pseudos[node.name] = true; + hasPseudo = true; + } + break; + + case 'FunctionalPseudo': + pseudos[node.name] = true; + hasPseudo = true; + break; + + case 'Negation': + pseudos.not = true; + hasPseudo = true; + break; + + case 'Identifier': + tagName = node.name; + break; + + case 'Attribute': + if (node.flags) { + pseudos['[' + node.flags + ']'] = true; + hasPseudo = true; + } + break; + + case 'Combinator': + tagName = '*'; + break; + } + }); + + simpleSelector.id = translate(simpleSelector); + simpleSelector.compareMarker = specificity(simpleSelector).toString(); + + if (scope) { + simpleSelector.compareMarker += ':' + scope; + } + + if (tagName !== '*') { + simpleSelector.compareMarker += ',' + tagName; + } + }); + + if (hasPseudo) { + node.pseudoSignature = Object.keys(pseudos).sort().join(','); + } +}; diff --git a/node_modules/csso/lib/compressor/restructure/prepare/specificity.js b/node_modules/csso/lib/compressor/restructure/prepare/specificity.js new file mode 100644 index 00000000..506c3373 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/prepare/specificity.js @@ -0,0 +1,48 @@ +module.exports = function specificity(simpleSelector) { + var A = 0; + var B = 0; + var C = 0; + + simpleSelector.sequence.each(function walk(data) { + switch (data.type) { + case 'SimpleSelector': + case 'Negation': + data.sequence.each(walk); + break; + + case 'Id': + A++; + break; + + case 'Class': + case 'Attribute': + case 'FunctionalPseudo': + B++; + break; + + case 'Identifier': + if (data.name !== '*') { + C++; + } + break; + + case 'PseudoElement': + C++; + break; + + case 'PseudoClass': + var name = data.name.toLowerCase(); + if (name === 'before' || + name === 'after' || + name === 'first-line' || + name === 'first-letter') { + C++; + } else { + B++; + } + break; + } + }); + + return [A, B, C]; +}; diff --git a/node_modules/csso/lib/compressor/restructure/utils.js b/node_modules/csso/lib/compressor/restructure/utils.js new file mode 100644 index 00000000..70c92c51 --- /dev/null +++ b/node_modules/csso/lib/compressor/restructure/utils.js @@ -0,0 +1,141 @@ +var hasOwnProperty = Object.prototype.hasOwnProperty; + +function isEqualLists(a, b) { + var cursor1 = a.head; + var cursor2 = b.head; + + while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) { + cursor1 = cursor1.next; + cursor2 = cursor2.next; + } + + return cursor1 === null && cursor2 === null; +} + +function isEqualDeclarations(a, b) { + var cursor1 = a.head; + var cursor2 = b.head; + + while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) { + cursor1 = cursor1.next; + cursor2 = cursor2.next; + } + + return cursor1 === null && cursor2 === null; +} + +function compareDeclarations(declarations1, declarations2) { + var result = { + eq: [], + ne1: [], + ne2: [], + ne2overrided: [] + }; + + var fingerprints = Object.create(null); + var declarations2hash = Object.create(null); + + for (var cursor = declarations2.head; cursor; cursor = cursor.next) { + declarations2hash[cursor.data.id] = true; + } + + for (var cursor = declarations1.head; cursor; cursor = cursor.next) { + var data = cursor.data; + + if (data.fingerprint) { + fingerprints[data.fingerprint] = data.value.important; + } + + if (declarations2hash[data.id]) { + declarations2hash[data.id] = false; + result.eq.push(data); + } else { + result.ne1.push(data); + } + } + + for (var cursor = declarations2.head; cursor; cursor = cursor.next) { + var data = cursor.data; + + if (declarations2hash[data.id]) { + // if declarations1 has overriding declaration, this is not a difference + // but take in account !important - prev should be equal or greater than follow + if (hasOwnProperty.call(fingerprints, data.fingerprint) && + Number(fingerprints[data.fingerprint]) >= Number(data.value.important)) { + result.ne2overrided.push(data); + } else { + result.ne2.push(data); + } + } + } + + return result; +} + +function addSelectors(dest, source) { + source.each(function(sourceData) { + var newStr = sourceData.id; + var cursor = dest.head; + + while (cursor) { + var nextStr = cursor.data.id; + + if (nextStr === newStr) { + return; + } + + if (nextStr > newStr) { + break; + } + + cursor = cursor.next; + } + + dest.insert(dest.createItem(sourceData), cursor); + }); + + return dest; +} + +// 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 = { + isEqualLists: isEqualLists, + isEqualDeclarations: isEqualDeclarations, + compareDeclarations: compareDeclarations, + addSelectors: addSelectors, + hasSimilarSelectors: hasSimilarSelectors, + unsafeToSkipNode: unsafeToSkipNode +}; |
