| var List = require('css-tree').List; |
| var generate = require('css-tree').generate; |
| var walk = require('css-tree').walk; |
| |
| 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.loc = null; |
| this.iehack = undefined; |
| this.sides = { |
| 'top': null, |
| 'right': null, |
| 'bottom': null, |
| 'left': null |
| }; |
| } |
| |
| TRBL.prototype.getValueSequence = function(declaration, count) { |
| var values = []; |
| var iehack = ''; |
| var hasBadValues = declaration.value.children.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 'HexColor': // color |
| case 'Number': |
| case 'Percentage': |
| break; |
| |
| case 'Function': |
| special = child.name; |
| break; |
| |
| case 'WhiteSpace': |
| return false; // ignore space |
| |
| default: |
| return true; // bad value |
| } |
| |
| values.push({ |
| node: child, |
| special: special, |
| important: declaration.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, declaration) { |
| function attemptToAdd() { |
| var sides = this.sides; |
| var side = SIDE[name]; |
| |
| if (side) { |
| if (side in sides === false) { |
| return false; |
| } |
| |
| var values = this.getValueSequence(declaration, 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(declaration, 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; |
| } |
| |
| // TODO: use it when we can refer to several points in source |
| // if (this.loc) { |
| // this.loc = { |
| // primary: this.loc, |
| // merged: declaration.loc |
| // }; |
| // } else { |
| // this.loc = declaration.loc; |
| // } |
| if (!this.loc) { |
| this.loc = declaration.loc; |
| } |
| |
| 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 = new List(); |
| var sides = this.sides; |
| var values = [ |
| sides.top, |
| sides.right, |
| sides.bottom, |
| sides.left |
| ]; |
| var stringValues = [ |
| generate(sides.top.node), |
| generate(sides.right.node), |
| generate(sides.bottom.node), |
| generate(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.appendData({ type: 'WhiteSpace', value: ' ' }); |
| } |
| |
| result.appendData(values[i].node); |
| } |
| |
| if (this.iehack) { |
| result.appendData({ type: 'WhiteSpace', value: ' ' }); |
| result.appendData({ |
| type: 'Identifier', |
| loc: null, |
| name: this.iehack |
| }); |
| } |
| |
| return { |
| type: 'Value', |
| loc: null, |
| children: result |
| }; |
| }; |
| |
| TRBL.prototype.getDeclaration = function() { |
| return { |
| type: 'Declaration', |
| loc: this.loc, |
| important: this.sides.top.important, |
| property: this.name, |
| value: this.getValue() |
| }; |
| }; |
| |
| function processRule(rule, shorts, shortDeclarations, lastShortSelector) { |
| var declarations = rule.block.children; |
| var selector = rule.prelude.children.first().id; |
| |
| rule.block.children.eachRight(function(declaration, item) { |
| var property = declaration.property; |
| |
| 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)) { |
| operation = REPLACE; |
| shorthand = new TRBL(key); |
| |
| // if can't parse value ignore it and break shorthand children |
| if (!shorthand.add(property, declaration)) { |
| 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(shorthand.getDeclaration()); |
| } else { |
| item.block.remove(item.item); |
| } |
| }); |
| } |
| |
| module.exports = function restructBlock(ast, indexer) { |
| var stylesheetMap = {}; |
| var shortDeclarations = []; |
| |
| walk(ast, { |
| visit: 'Rule', |
| reverse: true, |
| enter: function(node) { |
| var stylesheet = this.block || this.stylesheet; |
| var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id; |
| var ruleMap; |
| var shorts; |
| |
| if (!stylesheetMap.hasOwnProperty(stylesheet.id)) { |
| ruleMap = { |
| lastShortSelector: null |
| }; |
| stylesheetMap[stylesheet.id] = ruleMap; |
| } else { |
| ruleMap = stylesheetMap[stylesheet.id]; |
| } |
| |
| if (ruleMap.hasOwnProperty(ruleId)) { |
| shorts = ruleMap[ruleId]; |
| } else { |
| shorts = {}; |
| ruleMap[ruleId] = shorts; |
| } |
| |
| ruleMap.lastShortSelector = processRule.call(this, node, shorts, shortDeclarations, ruleMap.lastShortSelector); |
| } |
| }); |
| |
| processShorthands(shortDeclarations, indexer.declaration); |
| }; |