From: Timur Kristóf Date: Thu, 27 Feb 2014 23:36:57 +0000 (+0100) Subject: Shiny new property optimizer X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=69c228045824daeb59eb3c4f87b50a93a15c56a5;p=clean-css.git Shiny new property optimizer --- diff --git a/lib/properties/compact.js b/lib/properties/compact.js new file mode 100644 index 00000000..02f1809e --- /dev/null +++ b/lib/properties/compact.js @@ -0,0 +1,1290 @@ + +// The algorithm here is designed to optimize properties in a CSS selector block +// and output the smallest possible equivalent code. It is capable of +// +// 1. Merging properties that can override each other +// 2. Shorthanding properties when it makes sense +// +// Details are determined by `processable` - look at its comments to see how. +// This design has many benefits: +// +// * Can break up shorthands to their granular values +// * Deals with cases when a shorthand overrides more granular properties +// * Leaves fallbacks intact but merges equally understandable values +// * Removes default values from shorthand declarations +// * Opens up opportunities for further optimalizations because granular components of shorthands are much easier to compare/process individually +// + +module.exports = (function () { + + // Creates a property token with its default value + var makeDefaultProperty = function (prop, important, newValue) { + return { + prop: prop, + value: newValue || processable[prop].defaultValue, + isImportant: important + }; + }; + + // Creates an array of property tokens with their default values + var makeDefaultProperties = function (props, important) { + return props.map(function(prop) { return makeDefaultProperty(prop, important); }); + }; + + // Regexes used for stuff + var cssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(px|%|em|rem|in|cm|mm|ex|pt|pc|)|auto|inherit)'; + var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)'; + var cssFunctionVendorRegexStr = '\\-(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)'; + var cssFunctionAnyRegexStr = '(' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')'; + var cssUnitAnyRegexStr = '(' + cssUnitRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')'; + + // Validator + // NOTE: The point here is not semantical but syntactical validity + var validator = { + isValidHexColor: function (s) { + return (s.length === 4 || s.length === 7) && s[0] === '#'; + }, + isValidRgbaColor: function (s) { + s = s.split(' ').join(''); + return s.length > 0 && s.indexOf('rgba(') === 0 && s.indexOf(')') === s.length - 1; + }, + isValidHslaColor: function (s) { + s = s.split(' ').join(''); + return s.length > 0 && s.indexOf('hsla(') === 0 && s.indexOf(')') === s.length - 1; + }, + isValidNamedColor: function (s) { + // TODO: we don't really check if it's a valid color value, but allow any letters in it + return s !== 'auto' && (s === 'transparent' || s === 'inherit' || /^[a-zA-Z]+$/.test(s)); + }, + isValidColor: function (s) { + // http://www.w3schools.com/cssref/css_colors_legal.asp + return validator.isValidNamedColor(s) || validator.isValidHexColor(s) || validator.isValidRgbaColor(s) || validator.isValidHslaColor(s); + }, + isValidUrl: function (s) { + // NOTE: at this point all URLs are replaced with placeholders by clean-css, so we check for those placeholders + return s.indexOf('__ESCAPED_URL_CLEAN_CSS') === 0; + }, + isValidUnit: function (s) { + return new RegExp('^' + cssUnitAnyRegexStr + '$', 'gi').test(s); + }, + isValidUnitWithoutFunction: function (s) { + return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s); + }, + isValidFunctionWithoutVendorPrefix: function (s) { + return new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'gi').test(s); + }, + isValidFunctionWithVendorPrefix: function (s) { + return new RegExp('^' + cssFunctionVendorRegexStr + '$', 'gi').test(s); + }, + isValidFunction: function (s) { + return new RegExp('^' + cssFunctionAnyRegexStr + '$', 'gi').test(s); + }, + isValidBackgroundRepeat: function (s) { + return s === 'repeat' || s === 'no-repeat' || s === 'repeat-x' || s === 'repeat-y' || s === 'inherit'; + }, + isValidBackgroundAttachment: function (s) { + return s === 'inherit' || s === 'scroll' || s === 'fixed' || s === 'local'; + }, + isValidBackgroundPositionPart: function (s) { + if (s === 'center' || s === 'top' || s === 'bottom' || s === 'left' || s === 'right') + return true; + // LIMITATION: currently we don't support functions in here because otherwise we'd confuse things like linear-gradient() + // we need to figure out the complete list of functions that are allowed for units and then we can use isValidUnit here. + return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s); + }, + isValidBackgroundPosition: function (s) { + if (s === 'inherit') + return true; + return s.split(' ').every(function(p) { return validator.isValidBackgroundPositionPart(p); }); + }, + isValidListStyleType: function (s) { + return s === 'armenian' || s === 'circle' || s === 'cjk-ideographic' || s === 'decimal' || s === 'decimal-leading-zero' || s === 'disc' || s === 'georgian' || s === 'hebrew' || s === 'hiragana' || s === 'hiragana-iroha' || s === 'inherit' || s === 'katakana' || s === 'katakana-iroha' || s === 'lower-alpha' || s === 'lower-greek' || s === 'lower-latin' || s === 'lower-roman' || s === 'none' || s === 'square' || s === 'upper-alpha' || s === 'upper-latin' || s === 'upper-roman'; + }, + isValidListStylePosition: function (s) { + return s === 'inside' || s === 'outside' || s === 'inherit'; + }, + isValidOutlineColor: function (s) { + return s === 'invert' || validator.isValidColor(s) || validator.isValidVendorPrefixedValue(s); + }, + isValidOutlineStyle: function (s) { + return s === 'inherit' || s === 'hidden' || s === 'none' || s === 'dotted' || s === 'dashed' || s === 'solid' || s === 'double' || s === 'groove' || s === 'ridge' || s === 'inset' || s === 'outset'; + }, + isValidOutlineWidth: function (s) { + return validator.isValidUnit(s) || s === 'thin' || s === 'thick' || s === 'medium' || s === 'inherit'; + }, + isValidVendorPrefixedValue: function (s) { + return /^-([A-Za-z0-9]|-)*$/gi.test(s); + }, + areSameFunction: function (a, b) { + if (!validator.isValidFunction(a) || !validator.isValidFunction(b)) { + return false; + } + var f1name = a.substring(0, a.indexOf('(')); + var f2name = b.substring(0, b.indexOf('(')); + + return f1name === f2name; + } + }; + validator = Object.freeze(validator); + + // Merge functions + var canMerge = { + // Use when two tokens of the same property can always be merged + always: function () { + // NOTE: We could have (t1, t2) 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 (t1, t2) { + + return t1.value === t2.value; + }, + sameFunctionOrValue: function (t1, t2) { + // Functions with the same name can override each other + if (validator.areSameFunction(t1.value, t2.value)) { + return true; + } + + return t1.value === t2.value; + }, + // Use for properties containing CSS units (margin-top, padding-left, etc.) + unit: function (t1, t2) { + // 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(t2.value)) + return true; + if (validator.isValidUnitWithoutFunction(t1.value)) + return false; + + // Standard non-vendor-prefixed functions can override each other + if (validator.isValidFunctionWithoutVendorPrefix(t2.value) && validator.isValidFunctionWithoutVendorPrefix(t1.value)) { + return true; + } + + // Functions with the same name can override each other; same values can override each other + return canMerge.sameFunctionOrValue(t1, t2); + }, + // Use for color properties (color, background-color, border-color, etc.) + color: function (t1, t2) { + // 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(t2.value) || validator.isValidHexColor(t2.value)) + return true; + if (validator.isValidNamedColor(t1.value) || validator.isValidHexColor(t1.value)) + return false; + + // (rgba|hsla) + if (validator.isValidRgbaColor(t2.value) || validator.isValidHslColor(t2.value) || validator.isValidHslaColor(t2.value)) + return true; + if (validator.isValidRgbaColor(t1.value) || validator.isValidHslColor(t1.value) || validator.isValidHslaColor(t1.value)) + return false; + + // Functions with the same name can override each other; same values can override each other + return canMerge.sameFunctionOrValue(t1, t2); + }, + // Use for background-image + backgroundImage: function (t1, t2) { + // 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 (t2.value === 'none' || t2.value === 'inherit' || validator.isValidUrl(t2.value)) + return true; + if (t1.value === 'none' || t1.value === 'inherit' || validator.isValidUrl(t1.value)) + return false; + + // Functions with the same name can override each other; same values can override each other + return canMerge.sameFunctionOrValue(t1, t2); + } + // TODO: add more + }; + canMerge = Object.freeze(canMerge); + + // Functions for breaking up shorthands to components + var breakUp = { + // Use this for properties with 4 unit values (like margin or padding) + // NOTE: it handles shorter forms of these properties too (like, only 1, 2, or 3 units) + fourUnits: function (token) { + var descriptor = processable[token.prop]; + var result = []; + var splitval = token.value.match(new RegExp(cssUnitAnyRegexStr, 'gi')); + + 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 = { + prop: descriptor.components[i], + value: splitval[i], + isImportant: token.isImportant + }; + result.push(t); + } + + return result; + }, + // Breaks up a background property value + background: function (token) { + // Default values + var result = makeDefaultProperties(['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment'], token.isImportant); + var color = result[0], image = result[1], repeat = result[2], position = result[3], attachment = result[4]; + + // 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 = attachment.value = 'inherit'; + return result; + } + + // Break the background up into parts + var parts = token.value.split(' '); + if (parts.length === 0) { + return result; + } + + // The trick here is that we start going through the parts from the end, then stop after background repeat, + // then start from the from the beginning until we find a valid color value. What remains will be treated as background-image. + + var currentIndex = parts.length - 1; + var current = parts[currentIndex]; + // Attachment + if (validator.isValidBackgroundAttachment(current)) { + // Found attachment + attachment.value = current; + currentIndex--; + current = parts[currentIndex]; + } + // Position + var pos = parts[currentIndex - 1] + ' ' + parts[currentIndex]; + if (currentIndex >= 1 && validator.isValidBackgroundPosition(pos)) { + // Found position (containing two parts) + position.value = pos; + currentIndex -= 2; + current = parts[currentIndex]; + } + else if (currentIndex >= 0 && validator.isValidBackgroundPosition(current)) { + // Found position (containing just one part) + position.value = current; + currentIndex--; + current = parts[currentIndex]; + } + // Repeat + if (currentIndex >= 0 && validator.isValidBackgroundRepeat(current)) { + // Found repeat + repeat.value = current; + currentIndex--; + current = parts[currentIndex]; + } + // Color + var fromBeginning = 0; + if (validator.isValidColor(parts[0])) { + // Found color + color.value = parts[0]; + fromBeginning = 1; + } + // Image + image.value = (parts.splice(fromBeginning, currentIndex - fromBeginning + 1).join(' ')) || 'none'; + + return result; + }, + // Breaks up a list-style property value + listStyle: function (token) { + // Default values + var result = makeDefaultProperties(['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 = token.value.split(' '); + 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; + }, + // Breaks up outline + outline: function (token) { + // Default values + var result = makeDefaultProperties(['outline-color', 'outline-style', 'outline-width'], token.isImportant); + var color = result[0], style = result[1], width = result[2]; + + // 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 = token.value.split(' '), w; + + if (parts.length === 0) { + return result; + } + + if (parts.length >= 1) { + // Try to find outline-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 outline-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 outline-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 = Object.freeze(breakUp); + + // 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 = { + prop: prop, + value: tokens[0].value, + isImportant: isImportant, + 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) { + var result = { + prop: prop, + value: '', + isImportant: 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; + } + + result.value += ' ' + token.value; + } + + result.value = result.value.trim(); + if (!result.value) { + result.value = valueIfAllDefault; + } + + return result; + }, + // Handles the cases when some or all the fine-grained properties are set to inherit + takeCareOfInherit: function (innerFunc) { + return function (prop, tokens, isImportant) { + // 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]); + result2Shorthandable.push({ + prop: tokens[i].prop, + value: processable[tokens[i].prop].defaultValue, + isImportant: tokens[i].isImportant, + // Indicate that this property is irrelevant and its value can safely be set to anything else + isIrrelevant: true + }); + } + else { + nonInheritingTokens.push(tokens[i]); + result2Shorthandable.push(tokens[i]); + } + } + + // When all the tokens are 'inherit' + if (nonInheritingTokens.length === 0) { + return { + prop: prop, + value: 'inherit', + isImportant: isImportant + }; + } + // When some (but not all) of the tokens are 'inherit' + else if (inheritingTokens.length > 0) { + // Result 1. Shorthand just the inherit values and have it overridden with the non-inheriting ones + var result1 = [{ + prop: prop, + value: 'inherit', + isImportant: 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)].concat(inheritingTokens); + + // Return whichever is shorter + var dl1 = getDetokenizedLength(result1); + var dl2 = getDetokenizedLength(result2); + + return dl1 < dl2 ? result1 : result2; + } + // When none of tokens are 'inherit' + else { + return innerFunc(prop, tokens, isImportant); + } + }; + } + }; + putTogether = Object.freeze(putTogether); + + // 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. + // + // * canMerge: function (Default is canMerge.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 = { + 'margin': { + components: [ + 'margin-top', + 'margin-right', + 'margin-bottom', + 'margin-left' + ], + breakUp: breakUp.fourUnits, + putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits), + defaultValue: '0' + }, + 'margin-top': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'margin-right': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'margin-bottom': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'margin-left': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'padding': { + components: [ + 'padding-top', + 'padding-right', + 'padding-bottom', + 'padding-left' + ], + breakUp: breakUp.fourUnits, + putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits), + defaultValue: '0' + }, + 'padding-top': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'padding-right': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'padding-bottom': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'padding-left': { + defaultValue: '0', + canMerge: canMerge.unit + }, + 'background': { + components: [ + 'background-color', + 'background-image', + 'background-repeat', + 'background-position', + 'background-attachment' + ], + breakUp: breakUp.background, + putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults), + defaultValue: '0 0', + shortestValue: '0' + }, + 'color': { + canMerge: canMerge.color, + defaultValue: 'transparent', + shortestValue: 'red' + }, + 'background-color': { + // http://www.w3schools.com/cssref/pr_background-color.asp + canMerge: canMerge.color, + defaultValue: 'transparent', + shortestValue: 'red' + }, + 'background-image': { + // http://www.w3schools.com/cssref/pr_background-image.asp + canMerge: canMerge.backgroundImage, + defaultValue: 'none' + }, + 'background-repeat': { + // http://www.w3schools.com/cssref/pr_background-repeat.asp + canMerge: canMerge.always, + defaultValue: 'repeat' + }, + 'background-position': { + // http://www.w3schools.com/cssref/pr_background-position.asp + canMerge: canMerge.always, + defaultValue: '0 0', + shortestValue: '0' + }, + 'background-attachment': { + // http://www.w3schools.com/cssref/pr_background-attachment.asp + canMerge: canMerge.always, + defaultValue: 'scroll' + }, + 'list-style': { + // http://www.w3schools.com/cssref/pr_list-style.asp + components: [ + 'list-style-type', + 'list-style-position', + 'list-style-image' + ], + canMerge: canMerge.always, + breakUp: breakUp.listStyle, + putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults), + defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for
    + shortestValue: 'none' + }, + 'list-style-type' : { + // http://www.w3schools.com/cssref/pr_list-style-type.asp + canMerge: canMerge.always, + shortestValue: 'none', + defaultValue: '__hack' + // NOTE: we can't tell the real default value here, it's 'disc' for