From: Jakub Pawlowicz Date: Mon, 23 Mar 2015 20:36:07 +0000 (+0000) Subject: First step towards single tokenization. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=d72356a510980f274f18b1fddb72bd089d975cfe;p=clean-css.git First step towards single tokenization. * Removes tokenization from all steps in favour to single at the beginning. * Adds tokenization of properties value by value instead of all in one piece, e.g. `margin:0px 1px` gets tokenized into `margin`, `0px, and `1px`. * Full tokenization means more detailed source maps too. * Reworks override compactor and associated classes to use single tokenization. * Simplifies override compactor code so it's much easier to understand. * Adds loads of tests to override compactor and family. Caveats / in progress: * Shorthand compactor is turned off. * Inherit compacting is gone (but will be back). * Some multivalue background tests are failing. * There's no way to turn off compacting (possible performance regression). --- diff --git a/lib/properties/break-up.js b/lib/properties/break-up.js new file mode 100644 index 00000000..e585d9a2 --- /dev/null +++ b/lib/properties/break-up.js @@ -0,0 +1,306 @@ +var wrapSingle = require('./wrap-for-optimizing').single; +var validator = require('./validator'); + +var Splitter = require('../utils/splitter'); + +function _colorFilter(value) { + return value[0] == 'invert' || validator.isValidColor(value[0]); +} + +function _styleFilter(value) { + return value[0] != 'inherit' && validator.isValidStyle(value[0]); +} + +function _wrapDefault(name, property, compactable) { + var descriptor = compactable[name]; + if (descriptor.doubleValues && descriptor.defaultValue.length == 2) + return wrapSingle([[name, property.important], [descriptor.defaultValue[0]], [descriptor.defaultValue[1]]]); + else if (descriptor.doubleValues && descriptor.defaultValue.length == 1) + return wrapSingle([[name, property.important], [descriptor.defaultValue[0]]]); + else + return wrapSingle([[name, property.important], [descriptor.defaultValue]]); +} + +function _widthFilter(value) { + return value[0] != 'inherit' && validator.isValidWidth(value[0]); +} + +function background(property, compactable) { + var image = _wrapDefault('background-image', property, compactable); + var position = _wrapDefault('background-position', property, compactable); + var size = _wrapDefault('background-size', property, compactable); + var repeat = _wrapDefault('background-repeat', property, compactable); + var attachment = _wrapDefault('background-attachment', property, compactable); + var origin = _wrapDefault('background-origin', property, compactable); + var clip = _wrapDefault('background-clip', property, compactable); + var color = _wrapDefault('background-color', property, compactable); + var components = [image, position, size, repeat, attachment, origin, clip, color]; + var values = property.value; + + var positionSet = false; + var clipSet = false; + var originSet = false; + var repeatSet = false; + + if (property.value.length == 1 && property.value[0][0] == 'inherit') { + // NOTE: 'inherit' is not a valid value for background-attachment + color.value = image.value = repeat.value = position.value = size.value = origin.value = clip.value = property.value; + return components; + } + + for (var i = values.length - 1; i >= 0; i--) { + var value = values[i]; + + if (validator.isValidBackgroundAttachment(value[0])) { + attachment.value = [value]; + } else if (validator.isValidBackgroundBox(value[0])) { + if (clipSet) { + origin.value = [value]; + originSet = true; + } else { + clip.value = [value]; + clipSet = true; + } + } else if (validator.isValidBackgroundRepeat(value[0])) { + if (repeatSet) { + repeat.value.unshift(value); + } else { + repeat.value = [value]; + repeatSet = true; + } + } else if (validator.isValidBackgroundPositionPart(value[0]) || validator.isValidBackgroundSizePart(value[0])) { + if (i > 0) { + var previousValue = values[i - 1]; + + if (previousValue[0].indexOf('/') > 0) { + var twoParts = new Splitter('/').split(previousValue[0]); + // NOTE: we do this slicing as value may contain metadata too, like for source maps + size.value = [[twoParts.pop()].concat(previousValue.slice(1)), value]; + values[i - 1] = [twoParts.pop()].concat(previousValue.slice(1)); + } else if (i > 1 && values[i - 2] == '/') { + size.value = [previousValue, value]; + i -= 2; + } else if (values[i - 1] == '/') { + size.value = [value]; + } else { + if (!positionSet) + position.value = []; + + position.value.unshift(value); + positionSet = true; + } + } else { + if (!positionSet) + position.value = []; + + position.value.unshift(value); + positionSet = true; + } + } else if (validator.isValidBackgroundPositionAndSize(value[0])) { + var sizeValue = new Splitter('/').split(value[0]); + // NOTE: we do this slicing as value may contain metadata too, like for source maps + size.value = [[sizeValue.pop()].concat(value.slice(1))]; + position.value = [[sizeValue.pop()].concat(value.slice(1))]; + } else if ((color.value == compactable[color.name].defaultValue || color.value == 'none') && validator.isValidColor(value[0])) { + color.value = [value]; + } else if (validator.isValidUrl(value[0]) || validator.isValidFunction(value[0])) { + image.value = [value]; + } + } + + if (clipSet && !originSet) + origin.value = clip.value; + + return components; +} + +function borderRadius(property, compactable) { + var values = property.value; + var splitAt = -1; + + for (var i = 0, l = values.length; i < l; i++) { + if (values[i][0] == '/') { + splitAt = i; + break; + } + } + + if (splitAt == -1) + return fourValues(property, compactable); + + var target = _wrapDefault(property.name, property, compactable); + target.value = values.slice(0, splitAt); + target.components = fourValues(target, compactable); + + var remainder = _wrapDefault(property.name, property, compactable); + remainder.value = values.slice(splitAt + 1); + remainder.components = fourValues(remainder, compactable); + + for (var j = 0; j < 4; j++) { + target.components[j].value = [target.components[j].value, remainder.components[j].value]; + } + + return target.components; +} + +function fourValues(property, compactable) { + var componentNames = compactable[property.name].components; + var components = []; + var value = property.value; + + if (value.length < 1) + return []; + + if (value.length < 2) + value[1] = value[0]; + if (value.length < 3) + value[2] = value[0]; + if (value.length < 4) + value[3] = value[1]; + + for (var i = componentNames.length - 1; i >= 0; i--) { + var component = wrapSingle([[componentNames[i], property.important]]); + component.value = [value[i]]; + components.unshift(component); + } + + return components; +} + +function multipleValues(splitWith) { + return function (property, compactable) { + var splitsAt = []; + var values = property.value; + var i, j, l, m; + + // find split commas + for (i = 0, l = values.length; i < l; i++) { + if (values[i][0] == ',') + splitsAt.push(i); + } + + if (splitsAt.length === 0) + return splitWith(property, compactable); + + var splitComponents = []; + + // split over commas, and into components + for (i = 0, l = splitsAt.length; i <= l; i++) { + var from = i === 0 ? 0 : splitsAt[i - 1] + 1; + var to = i < l ? splitsAt[i] : values.length; + + var _property = _wrapDefault(property.name, property, compactable); + _property.value = values.slice(from, to); + + splitComponents.push(splitWith(_property, compactable)); + } + + var components = splitComponents[0]; + + // group component values from each split + for (i = 0, l = components.length; i < l; i++) { + components[i].value = [components[i].value]; + components[i].multiplex = true; + + for (j = 1, m = splitComponents.length; j < m; j++) { + components[i].value.push(splitComponents[j][i].value); + } + } + + return components; + }; +} + +function listStyle(property, compactable) { + var type = _wrapDefault('list-style-type', property, compactable); + var position = _wrapDefault('list-style-position', property, compactable); + var image = _wrapDefault('list-style-image', property, compactable); + var components = [type, position, image]; + + if (property.value.length == 1 && property.value[0][0] == 'inherit') { + type.value = position.value = image.value = [property.value[0]]; + return components; + } + + var values = property.value; + var index = 0; + + if (index < values.length && validator.isValidListStyleType(values[index][0])) + type.value = [values[index++]]; + if (index < values.length && validator.isValidListStylePosition(values[index][0])) + position.value = [values[index++]]; + if (index < values.length) + image.value = [values[index]]; + + return components; +} + +function widthStyleColor(property, compactable) { + var descriptor = compactable[property.name]; + var components = [ + _wrapDefault(descriptor.components[0], property, compactable), + _wrapDefault(descriptor.components[1], property, compactable), + _wrapDefault(descriptor.components[2], property, compactable) + ]; + var color, style, width; + + for (var i = 0; i < 3; i++) { + var component = components[i]; + + if (component.name.indexOf('color') > 0) + color = component; + else if (component.name.indexOf('style') > 0) + style = component; + else + width = component; + } + + if ((property.value.length == 1 && property.value[0][0] == 'inherit') || + (property.value.length == 3 && property.value[0][0] == 'inherit' && property.value[1][0] == 'inherit' && property.value[2][0] == 'inherit')) { + color.value = style.value = width.value = [property.value[0]]; + return components; + } + + var values = property.value.slice(0); + var match, matches; + + // 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 + + if (values.length > 0) { + matches = values.filter(_widthFilter); + match = matches.length > 1 && matches[0] == 'none' ? matches[1] : matches[0]; + if (match) { + width.value = [match]; + values.splice(values.indexOf(match), 1); + } + } + + if (values.length > 0) { + match = values.filter(_styleFilter)[0]; + if (match) { + style.value = [match]; + values.splice(values.indexOf(match), 1); + } + } + + if (values.length > 0) { + match = values.filter(_colorFilter)[0]; + if (match) { + color.value = [match]; + values.splice(values.indexOf(match), 1); + } + } + + return components; +} + +module.exports = { + background: background, + border: widthStyleColor, + borderRadius: borderRadius, + fourValues: fourValues, + listStyle: listStyle, + multipleValues: multipleValues, + outline: widthStyleColor +}; diff --git a/lib/properties/can-override.js b/lib/properties/can-override.js new file mode 100644 index 00000000..ffccfd2e --- /dev/null +++ b/lib/properties/can-override.js @@ -0,0 +1,126 @@ +var validator = require('./validator'); + +// 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. + +function _valueOf(property) { + return Array.isArray(property.value[0]) ? property.value[0][0] : property.value[0]; +} + +// Use when two tokens of the same property can always be merged +function always() { + return true; +} + +function backgroundImage(property1, property2) { + // 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) + var image1 = _valueOf(property1); + var image2 = _valueOf(property2); + + if (image2 == 'none' || image2 == 'inherit' || validator.isValidUrl(image2)) + return true; + if (image1 == 'none' || image1 == 'inherit' || validator.isValidUrl(image1)) + return false; + + // Functions with the same name can override each other; same values can override each other + return sameFunctionOrValue(property1, property2); +} + +function border(property1, property2) { + return color(property1.components[2], property2.components[2]); +} + +// Use for color properties (color, background-color, border-color, etc.) +function color(property1, property2) { + // 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 + + var color1 = _valueOf(property1); + var color2 = _valueOf(property2); + + // (hex | named) + if (validator.isValidNamedColor(color2) || validator.isValidHexColor(color2)) + return true; + if (validator.isValidNamedColor(color1) || validator.isValidHexColor(color1)) + return false; + + // (rgba|hsla) + if (validator.isValidRgbaColor(color2) || validator.isValidHslaColor(color2)) + return true; + if (validator.isValidRgbaColor(color1) || validator.isValidHslaColor(color1)) + return false; + + // Functions with the same name can override each other; same values can override each other + return sameFunctionOrValue(property1, property2); +} + +function twoOptionalFunctions(property1, property2) { + var value1 = _valueOf(property1); + var value2 = _valueOf(property2); + + return !(validator.isValidFunction(value1) ^ validator.isValidFunction(value2)); +} + +function sameValue(property1, property2) { + var value1 = _valueOf(property1); + var value2 = _valueOf(property2); + + return value1 === value2; +} + +function sameFunctionOrValue(property1, property2) { + var value1 = _valueOf(property1); + var value2 = _valueOf(property2); + + // Functions with the same name can override each other + if (validator.areSameFunction(value1, value2)) + return true; + + return value1 === value2; +} + +// Use for properties containing CSS units (margin-top, padding-left, etc.) +function unit(property1, property2) { + // 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 + var value1 = _valueOf(property1); + var value2 = _valueOf(property2); + + if (validator.isValidAndCompatibleUnitWithoutFunction(value1) && !validator.isValidAndCompatibleUnitWithoutFunction(value2)) + return false; + + if (validator.isValidUnitWithoutFunction(value2)) + return true; + if (validator.isValidUnitWithoutFunction(value1)) + return false; + + // Standard non-vendor-prefixed functions can override each other + if (validator.isValidFunctionWithoutVendorPrefix(value2) && validator.isValidFunctionWithoutVendorPrefix(value1)) { + return true; + } + + // Functions with the same name can override each other; same values can override each other + return sameFunctionOrValue(property1, property2); +} + +module.exports = { + always: always, + backgroundImage: backgroundImage, + border: border, + color: color, + sameValue: sameValue, + sameFunctionOrValue: sameFunctionOrValue, + twoOptionalFunctions: twoOptionalFunctions, + unit: unit +}; diff --git a/lib/properties/compactable.js b/lib/properties/compactable.js new file mode 100644 index 00000000..47bc0a33 --- /dev/null +++ b/lib/properties/compactable.js @@ -0,0 +1,275 @@ +// Contains the interpretation of CSS properties, as used by the property optimizer + +var breakUp = require('./break-up'); +var canOverride = require('./can-override'); +var restore = require('./restore'); + +// 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. +// +// * restore: function (Only specify for shorthand properties.) +// Puts the shorthand together from its components. +// +var compactable = { + 'color': { + canOverride: canOverride.color, + defaultValue: 'transparent', + shortestValue: 'red' + }, + 'background': { + components: [ + 'background-image', + 'background-position', + 'background-size', + 'background-repeat', + 'background-attachment', + 'background-origin', + 'background-clip', + 'background-color' + ], + breakUp: breakUp.multipleValues(breakUp.background), + defaultValue: '0 0', + restore: restore.multipleValues(restore.background), + shortestValue: '0', + shorthand: true + }, + 'background-clip': { + canOverride: canOverride.always, + defaultValue: 'border-box', + shortestValue: 'border-box' + }, + 'background-color': { + canOverride: canOverride.color, + defaultValue: 'transparent', + multiplexLastOnly: true, + nonMergeableValue: 'none', + shortestValue: 'red' + }, + 'background-image': { + canOverride: canOverride.backgroundImage, + defaultValue: 'none' + }, + 'background-origin': { + canOverride: canOverride.always, + defaultValue: 'padding-box', + shortestValue: 'border-box' + }, + 'background-repeat': { + canOverride: canOverride.always, + defaultValue: ['repeat'], + doubleValues: true + }, + 'background-position': { + canOverride: canOverride.always, + defaultValue: ['0', '0'], + doubleValues: true, + shortestValue: '0' + }, + 'background-size': { + canOverride: canOverride.always, + defaultValue: ['auto'], + doubleValues: true, + shortestValue: '0 0' + }, + 'background-attachment': { + canOverride: canOverride.always, + defaultValue: 'scroll' + }, + 'border': { + breakUp: breakUp.border, + canOverride: canOverride.border, + components: [ + 'border-width', + 'border-style', + 'border-color' + ], + defaultValue: 'none', + restore: restore.withoutDefaults, + shorthand: true + }, + 'border-color': { + canOverride: canOverride.color, + defaultValue: 'none', + shorthand: true + }, + 'border-style': { + canOverride: canOverride.always, + defaultValue: 'none', + shorthand: true + }, + 'border-width': { + canOverride: canOverride.unit, + defaultValue: 'medium', + shortestValue: '0', + shorthand: true + }, + 'list-style': { + components: [ + 'list-style-type', + 'list-style-position', + 'list-style-image' + ], + canOverride: canOverride.always, + breakUp: breakUp.listStyle, + restore: restore.withoutDefaults, + defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for
    + shortestValue: 'none', + shorthand: true + }, + 'list-style-type' : { + canOverride: canOverride.always, + defaultValue: '__hack', + // NOTE: we can't tell the real default value here, it's 'disc' for