// Contains the interpretation of CSS properties, as used by the property optimizer module.exports = (function () { var tokenModule = require('./token'); var validator = require('./validator'); var Splitter = require('../text/splitter'); // Functions that decide what value can override what. // The main purpose is to disallow removing CSS fallbacks. // A separate implementation is needed for every different kind of CSS property. // ----- // The generic idea is that properties that have wider browser support are 'more understandable' // than others and that 'less understandable' values can't override more understandable ones. var canOverride = { // Use when two tokens of the same property can always be merged always: function () { // NOTE: We could have (val1, val2) parameters here but jshint complains because we don't use them return true; }, // Use when two tokens of the same property can only be merged if they have the same value sameValue: function(val1, val2) { return val1 === val2; }, sameFunctionOrValue: function(val1, val2) { // Functions with the same name can override each other if (validator.areSameFunction(val1, val2)) { return true; } return val1 === val2; }, // Use for properties containing CSS units (margin-top, padding-left, etc.) unit: function(val1, val2) { // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa // Understandability: (unit without functions) > (same functions | standard functions) > anything else // NOTE: there is no point in having different vendor-specific functions override each other or standard functions, // or having standard functions override vendor-specific functions, but standard functions can override each other // NOTE: vendor-specific property values are not taken into consideration here at the moment if (validator.isValidUnitWithoutFunction(val2)) return true; if (validator.isValidUnitWithoutFunction(val1)) return false; // Standard non-vendor-prefixed functions can override each other if (validator.isValidFunctionWithoutVendorPrefix(val2) && validator.isValidFunctionWithoutVendorPrefix(val1)) { return true; } // Functions with the same name can override each other; same values can override each other return canOverride.sameFunctionOrValue(val1, val2); }, // Use for color properties (color, background-color, border-color, etc.) color: function(val1, val2) { // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa // Understandability: (hex | named) > (rgba | hsla) > (same function name) > anything else // NOTE: at this point rgb and hsl are replaced by hex values by clean-css // (hex | named) if (validator.isValidNamedColor(val2) || validator.isValidHexColor(val2)) return true; if (validator.isValidNamedColor(val1) || validator.isValidHexColor(val1)) return false; // (rgba|hsla) if (validator.isValidRgbaColor(val2) || validator.isValidHslaColor(val2)) return true; if (validator.isValidRgbaColor(val1) || validator.isValidHslaColor(val1)) return false; // Functions with the same name can override each other; same values can override each other return canOverride.sameFunctionOrValue(val1, val2); }, // Use for background-image backgroundImage: function(val1, val2) { // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa // Understandability: (none | url | inherit) > (same function) > (same value) // (none | url) if (val2 === 'none' || val2 === 'inherit' || validator.isValidUrl(val2)) return true; if (val1 === 'none' || val1 === 'inherit' || validator.isValidUrl(val1)) return false; // Functions with the same name can override each other; same values can override each other return canOverride.sameFunctionOrValue(val1, val2); }, border: function(val1, val2) { var brokenUp1 = breakUp.border(Token.tokenizeOne(val1)); var brokenUp2 = breakUp.border(Token.tokenizeOne(val2)); return canOverride.color(brokenUp1[2].value, brokenUp2[2].value); } }; canOverride = Object.freeze(canOverride); // Functions for breaking up shorthands to components var breakUp = {}; breakUp.takeCareOfFourValues = function (splitfunc) { return function (token) { var descriptor = processable[token.prop]; var result = []; var splitval = splitfunc(token.value); if (splitval.length === 0 || (splitval.length < descriptor.components.length && descriptor.components.length > 4)) { // This token is malformed and we have no idea how to fix it. So let's just keep it intact return [token]; } // Fix those that we do know how to fix if (splitval.length < descriptor.components.length && splitval.length < 2) { // foo{margin:1px} -> foo{margin:1px 1px} splitval[1] = splitval[0]; } if (splitval.length < descriptor.components.length && splitval.length < 3) { // foo{margin:1px 2px} -> foo{margin:1px 2px 1px} splitval[2] = splitval[0]; } if (splitval.length < descriptor.components.length && splitval.length < 4) { // foo{margin:1px 2px 3px} -> foo{margin:1px 2px 3px 2px} splitval[3] = splitval[1]; } // Now break it up to its components for (var i = 0; i < descriptor.components.length; i++) { var t = new Token(descriptor.components[i], splitval[i], token.isImportant); result.push(t); } return result; }; }; // Use this when you simply want to break up four values along spaces breakUp.fourBySpaces = breakUp.takeCareOfFourValues(function (val) { return new Splitter(' ').split(val).filter(function (v) { return v; }); }); // Breaks up a background property value breakUp.commaSeparatedMulitpleValues = function (splitfunc) { return function (token) { if (token.value.indexOf(',') === -1) return splitfunc(token); var values = new Splitter(',').split(token.value); var components = []; for (var i = 0, l = values.length; i < l; i++) { token.value = values[i]; components.push(splitfunc(token)); } for (var j = 0, m = components[0].length; j < m; j++) { for (var k = 0, n = components.length, newValues = []; k < n; k++) { newValues.push(components[k][j].value); } components[0][j].value = newValues.join(','); } return components[0]; }; }; breakUp.background = function (token) { // Default values var result = Token.makeDefaults(['background-image', 'background-position', 'background-size', 'background-repeat', 'background-attachment', 'background-color'], token.isImportant); var image = result[0]; var position = result[1]; var size = result[2]; var repeat = result[3]; var attachment = result[4]; var color = result[5]; var positionSet = false; // Take care of inherit if (token.value === 'inherit') { // NOTE: 'inherit' is not a valid value for background-attachment so there we'll leave the default value color.value = image.value = repeat.value = position.value = size.value = attachment.value = 'inherit'; return result; } // Break the background up into parts var parts = new Splitter(' ').split(token.value); if (parts.length === 0) return result; // Iterate over all parts and try to fit them into positions for (var i = parts.length - 1; i >= 0; i--) { var currentPart = parts[i]; if (validator.isValidBackgroundAttachment(currentPart)) { attachment.value = currentPart; } else if (validator.isValidBackgroundRepeat(currentPart)) { repeat.value = currentPart; } else if (validator.isValidBackgroundPositionPart(currentPart) || validator.isValidBackgroundSizePart(currentPart)) { if (i > 0) { var previousPart = parts[i - 1]; if (previousPart.indexOf('/') > 0) { var twoParts = new Splitter('/').split(previousPart); size.value = twoParts.pop() + ' ' + currentPart; parts[i - 1] = twoParts.pop(); } else if (i > 1 && parts[i - 2] == '/') { size.value = previousPart + ' ' + currentPart; i -= 2; } else if (parts[i - 1] == '/') { size.value = currentPart; } else { position.value = currentPart + (positionSet ? ' ' + position.value : ''); positionSet = true; } } else { position.value = currentPart + (positionSet ? ' ' + position.value : ''); positionSet = true; } } else if (validator.isValidBackgroundPositionAndSize(currentPart)) { var sizeValue = new Splitter('/').split(currentPart); size.value = sizeValue.pop(); position.value = sizeValue.pop(); } else if ((color.value == processable[color.prop].defaultValue || color.value == 'none') && validator.isValidColor(currentPart)) { color.value = currentPart; } else if (validator.isValidUrl(currentPart) || validator.isValidFunction(currentPart)) { image.value = currentPart; } } return result; }; // Breaks up a list-style property value breakUp.listStyle = function (token) { // Default values var result = Token.makeDefaults(['list-style-type', 'list-style-position', 'list-style-image'], token.isImportant); var type = result[0], position = result[1], image = result[2]; if (token.value === 'inherit') { type.value = position.value = image.value = 'inherit'; return result; } var parts = new Splitter(' ').split(token.value); var ci = 0; // Type if (ci < parts.length && validator.isValidListStyleType(parts[ci])) { type.value = parts[ci]; ci++; } // Position if (ci < parts.length && validator.isValidListStylePosition(parts[ci])) { position.value = parts[ci]; ci++; } // Image if (ci < parts.length) { image.value = parts.splice(ci, parts.length - ci + 1).join(' '); } return result; }; breakUp._widthStyleColor = function(token, prefix, order) { // Default values var components = order.map(function(prop) { return prefix + '-' + prop; }); var result = Token.makeDefaults(components, token.isImportant); var color = result[order.indexOf('color')]; var style = result[order.indexOf('style')]; var width = result[order.indexOf('width')]; // Take care of inherit if (token.value === 'inherit' || token.value === 'inherit inherit inherit') { color.value = style.value = width.value = 'inherit'; return result; } // NOTE: usually users don't follow the required order of parts in this shorthand, // so we'll try to parse it caring as little about order as possible var parts = new Splitter(' ').split(token.value), w; if (parts.length === 0) { return result; } if (parts.length >= 1) { // Try to find -width, excluding inherit because that can be anything w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineWidth(p); }); if (w.length) { width.value = w[0]; parts.splice(parts.indexOf(w[0]), 1); } } if (parts.length >= 1) { // Try to find -style, excluding inherit because that can be anything w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineStyle(p); }); if (w.length) { style.value = w[0]; parts.splice(parts.indexOf(w[0]), 1); } } if (parts.length >= 1) { // Find -color but this time can catch inherit w = parts.filter(function(p) { return validator.isValidOutlineColor(p); }); if (w.length) { color.value = w[0]; parts.splice(parts.indexOf(w[0]), 1); } } return result; }; breakUp.outline = function(token) { return breakUp._widthStyleColor(token, 'outline', ['color', 'style', 'width']); }; breakUp.border = function(token) { return breakUp._widthStyleColor(token, 'border', ['width', 'style', 'color']); }; breakUp.borderRadius = function(token) { var parts = token.value.split('/'); if (parts.length == 1) return breakUp.fourBySpaces(token); var horizontalPart = token.clone(); var verticalPart = token.clone(); horizontalPart.value = parts[0]; verticalPart.value = parts[1]; var horizontalBreakUp = breakUp.fourBySpaces(horizontalPart); var verticalBreakUp = breakUp.fourBySpaces(verticalPart); for (var i = 0; i < 4; i++) { horizontalBreakUp[i].value = [horizontalBreakUp[i].value, verticalBreakUp[i].value]; } return horizontalBreakUp; }; // Contains functions that can put together shorthands from their components // NOTE: correct order of tokens is assumed inside these functions! var putTogether = { // Use this for properties which have four unit values (margin, padding, etc.) // NOTE: optimizes to shorter forms too (that only specify 1, 2, or 3 values) fourUnits: function (prop, tokens, isImportant) { // See about irrelevant tokens // NOTE: This will enable some crazy optimalizations for us. if (tokens[0].isIrrelevant) tokens[0].value = tokens[2].value; if (tokens[2].isIrrelevant) tokens[2].value = tokens[0].value; if (tokens[1].isIrrelevant) tokens[1].value = tokens[3].value; if (tokens[3].isIrrelevant) tokens[3].value = tokens[1].value; if (tokens[0].isIrrelevant && tokens[2].isIrrelevant) { if (tokens[1].value === tokens[3].value) tokens[0].value = tokens[2].value = tokens[1].value; else tokens[0].value = tokens[2].value = '0'; } if (tokens[1].isIrrelevant && tokens[3].isIrrelevant) { if (tokens[0].value === tokens[2].value) tokens[1].value = tokens[3].value = tokens[0].value; else tokens[1].value = tokens[3].value = '0'; } var result = new Token(prop, tokens[0].value, isImportant); result.granularValues = []; result.granularValues[tokens[0].prop] = tokens[0].value; result.granularValues[tokens[1].prop] = tokens[1].value; result.granularValues[tokens[2].prop] = tokens[2].value; result.granularValues[tokens[3].prop] = tokens[3].value; // If all of them are irrelevant if (tokens[0].isIrrelevant && tokens[1].isIrrelevant && tokens[2].isIrrelevant && tokens[3].isIrrelevant) { result.value = processable[prop].shortestValue || processable[prop].defaultValue; return result; } // 1-value short form: all four components are equal if (tokens[0].value === tokens[1].value && tokens[0].value === tokens[2].value && tokens[0].value === tokens[3].value) { return result; } result.value += ' ' + tokens[1].value; // 2-value short form: first and third; second and fourth values are equal if (tokens[0].value === tokens[2].value && tokens[1].value === tokens[3].value) { return result; } result.value += ' ' + tokens[2].value; // 3-value short form: second and fourth values are equal if (tokens[1].value === tokens[3].value) { return result; } // 4-value form (none of the above optimalizations could be accomplished) result.value += ' ' + tokens[3].value; return result; }, // Puts together the components by spaces and omits default values (this is the case for most shorthands) bySpacesOmitDefaults: function (prop, tokens, isImportant, meta) { var result = new Token(prop, '', isImportant); // Get irrelevant tokens var irrelevantTokens = tokens.filter(function (t) { return t.isIrrelevant; }); // If every token is irrelevant, return shortest possible value, fallback to default value if (irrelevantTokens.length === tokens.length) { result.isIrrelevant = true; result.value = processable[prop].shortestValue || processable[prop].defaultValue; return result; } // This will be the value of the shorthand if all the components are default var valueIfAllDefault = processable[prop].defaultValue; // Go through all tokens and concatenate their values as necessary for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; // Set granular value so that other parts of the code can use this for optimalization opportunities result.granularValues = result.granularValues || { }; result.granularValues[token.prop] = token.value; // Use irrelevant tokens for optimalization opportunity if (token.isIrrelevant) { // Get shortest possible value, fallback to default value var tokenShortest = processable[token.prop].shortestValue || processable[token.prop].defaultValue; // If the shortest possible value of this token is shorter than the default value of the shorthand, use it instead if (tokenShortest.length < valueIfAllDefault.length) { valueIfAllDefault = tokenShortest; } } // Omit default / irrelevant value if (token.isIrrelevant || (processable[token.prop] && processable[token.prop].defaultValue === token.value)) { continue; } if (meta && meta.partsCount && meta.position < meta.partsCount - 1 && processable[token.prop].multiValueLastOnly) continue; var requiresPreceeding = processable[token.prop].shorthandFollows; if (requiresPreceeding && (tokens[i - 1].value == processable[requiresPreceeding].defaultValue)) { result.value += ' ' + tokens[i - 1].value; } result.value += (processable[token.prop].prefixShorthandValueWith || ' ') + token.value; } result.value = result.value.trim(); if (!result.value) { result.value = valueIfAllDefault; } return result; }, commaSeparatedMulitpleValues: function (assembleFunction) { return function(prop, tokens, isImportant) { var tokenSplitLengths = tokens.map(function (token) { return new Splitter(',').split(token.value).length; }); var partsCount = Math.max.apply(Math, tokenSplitLengths); if (partsCount == 1) return assembleFunction(prop, tokens, isImportant); var merged = []; for (var i = 0; i < partsCount; i++) { merged.push([]); for (var j = 0; j < tokens.length; j++) { var split = new Splitter(',').split(tokens[j].value); merged[i].push(split[i] || split[0]); } } var mergedValues = []; var firstProcessed; for (i = 0; i < partsCount; i++) { var newTokens = []; for (var k = 0, n = merged[i].length; k < n; k++) { var newToken = tokens[k].clone(); newToken.value = merged[i][k]; newTokens.push(newToken); } var meta = { partsCount: partsCount, position: i }; var processed = assembleFunction(prop, newTokens, isImportant, meta); mergedValues.push(processed.value); if (!firstProcessed) firstProcessed = processed; } firstProcessed.value = mergedValues.join(','); return firstProcessed; }; }, // Handles the cases when some or all the fine-grained properties are set to inherit takeCareOfInherit: function (innerFunc) { return function (prop, tokens, isImportant, meta) { // Filter out the inheriting and non-inheriting tokens in one iteration var inheritingTokens = []; var nonInheritingTokens = []; var result2Shorthandable = []; var i; for (i = 0; i < tokens.length; i++) { if (tokens[i].value === 'inherit') { inheritingTokens.push(tokens[i]); // Indicate that this property is irrelevant and its value can safely be set to anything else var r2s = new Token(tokens[i].prop, tokens[i].isImportant); r2s.isIrrelevant = true; result2Shorthandable.push(r2s); } else { nonInheritingTokens.push(tokens[i]); result2Shorthandable.push(tokens[i]); } } if (nonInheritingTokens.length === 0) { // When all the tokens are 'inherit' return new Token(prop, 'inherit', isImportant); } else if (inheritingTokens.length > 0) { // When some (but not all) of the tokens are 'inherit' // Result 1. Shorthand just the inherit values and have it overridden with the non-inheriting ones var result1 = [new Token(prop, 'inherit', isImportant)].concat(nonInheritingTokens); // Result 2. Shorthand every non-inherit value and then have it overridden with the inheriting ones var result2 = [innerFunc(prop, result2Shorthandable, isImportant, meta)].concat(inheritingTokens); // Return whichever is shorter var dl1 = Token.getDetokenizedLength(result1); var dl2 = Token.getDetokenizedLength(result2); return dl1 < dl2 ? result1 : result2; } else { // When none of tokens are 'inherit' return innerFunc(prop, tokens, isImportant, meta); } }; }, borderRadius: function (prop, tokens, isImportant) { var verticalTokens = []; var newTokens = []; for (var i = 0, l = tokens.length; i < l; i++) { var token = tokens[i]; var newToken = token.clone(); newTokens.push(newToken); if (!Array.isArray(token.value)) continue; if (token.value.length > 1) { verticalTokens.push({ prop: token.prop, value: token.value[1], isImportant: token.isImportant }); } newToken.value = token.value[0]; } var result = putTogether.takeCareOfInherit(putTogether.fourUnits)(prop, newTokens, isImportant); if (verticalTokens.length > 0) { var verticalResult = putTogether.takeCareOfInherit(putTogether.fourUnits)(prop, verticalTokens, isImportant); if (result.value != verticalResult.value) result.value += '/' + verticalResult.value; } return result; } }; // Properties to process // Extend this object in order to add support for more properties in the optimizer. // // Each key in this object represents a CSS property and should be an object. // Such an object contains properties that describe how the represented CSS property should be handled. // Possible options: // // * components: array (Only specify for shorthand properties.) // Contains the names of the granular properties this shorthand compacts. // // * canOverride: function (Default is canOverride.sameValue - meaning that they'll only be merged if they have the same value.) // Returns whether two tokens of this property can be merged with each other. // This property has no meaning for shorthands. // // * defaultValue: string // Specifies the default value of the property according to the CSS standard. // For shorthand, this is used when every component is set to its default value, therefore it should be the shortest possible default value of all the components. // // * shortestValue: string // Specifies the shortest possible value the property can possibly have. // (Falls back to defaultValue if unspecified.) // // * breakUp: function (Only specify for shorthand properties.) // Breaks the shorthand up to its components. // // * putTogether: function (Only specify for shorthand properties.) // Puts the shorthand together from its components. // var processable = { 'color': { canOverride: canOverride.color, defaultValue: 'transparent', shortestValue: 'red' }, // background ------------------------------------------------------------------------------ 'background': { components: [ 'background-image', 'background-position', 'background-size', 'background-repeat', 'background-attachment', 'background-color' ], breakUp: breakUp.commaSeparatedMulitpleValues(breakUp.background), putTogether: putTogether.commaSeparatedMulitpleValues( putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults) ), defaultValue: '0 0', shortestValue: '0' }, 'background-color': { canOverride: canOverride.color, defaultValue: 'transparent', multiValueLastOnly: true, shortestValue: 'red' }, 'background-image': { canOverride: canOverride.backgroundImage, defaultValue: 'none' }, 'background-repeat': { canOverride: canOverride.always, defaultValue: 'repeat' }, 'background-position': { canOverride: canOverride.always, defaultValue: '0 0', shortestValue: '0' }, 'background-size': { canOverride: canOverride.always, defaultValue: 'auto', shortestValue: '0 0', prefixShorthandValueWith: '/', shorthandFollows: 'background-position' }, 'background-attachment': { canOverride: canOverride.always, defaultValue: 'scroll' }, 'border': { breakUp: breakUp.border, canOverride: canOverride.border, components: [ 'border-width', 'border-style', 'border-color' ], defaultValue: 'none', putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults) }, 'border-color': { canOverride: canOverride.color, defaultValue: 'none' }, 'border-style': { canOverride: canOverride.always, defaultValue: 'none' }, 'border-width': { canOverride: canOverride.unit, defaultValue: 'medium', shortestValue: '0' }, // list-style ------------------------------------------------------------------------------ 'list-style': { components: [ 'list-style-type', 'list-style-position', 'list-style-image' ], canOverride: canOverride.always, breakUp: breakUp.listStyle, putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults), defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for