Fixes #130 - better code modularity.
authorGoalSmashers <jakub@goalsmashers.com>
Wed, 28 Aug 2013 10:27:16 +0000 (12:27 +0200)
committerGoalSmashers <jakub@goalsmashers.com>
Thu, 29 Aug 2013 14:28:37 +0000 (16:28 +0200)
* Moves url, free text, and comments' processing out of the main module.
* Moves import inliner, and shorthand notations out of the main module.
* Moves color processing out of the main module.

13 files changed:
History.md
lib/clean.js
lib/color-shortening.js [deleted file]
lib/colors/hsl-to-hex.js [new file with mode: 0644]
lib/colors/long-to-short-hex.js [new file with mode: 0644]
lib/colors/rgb-to-hex.js [new file with mode: 0644]
lib/colors/shortener.js [new file with mode: 0644]
lib/imports/inliner.js [new file with mode: 0644]
lib/properties/shorthand-notations.js [new file with mode: 0644]
lib/text/comments.js [new file with mode: 0644]
lib/text/free.js [new file with mode: 0644]
lib/text/urls.js [new file with mode: 0644]
test/unit-test.js

index 905d568..acba052 100644 (file)
@@ -1,6 +1,7 @@
 1.1.0 / 2013-xx-xx (UNRELEASED)
 ===================
 
+* Fixed issue [#130](https://github.com/GoalSmashers/clean-css/issues/130) - better code modularity.
 * Fixed issue [#65](https://github.com/GoalSmashers/clean-css/issues/65) - full color name / hex shortening.
 
 1.0.12 / 2013-07-19
index ab39328..aca621a 100644 (file)
@@ -5,19 +5,20 @@
  * Copyright (C) 2011-2013 GoalSmashers.com
  */
 
-var fs = require('fs');
-var path = require('path');
-var existsSync = fs.existsSync || path.existsSync;
+var ColorShortener = require('./colors/shortener');
+var ColorHSLToHex = require('./colors/hsl-to-hex');
+var ColorRGBToHex = require('./colors/rgb-to-hex');
+var ColorLongToShortHex = require('./colors/long-to-short-hex');
 
-var colorShortening = require('../lib/color-shortening');
+var ShorthandNotations = require('./properties/shorthand-notations');
+var ImportInliner = require('./imports/inliner');
+
+var CommentsProcessor = require('./text/comments');
+var FreeTextProcessor = require('./text/free');
+var UrlsProcessor = require('./text/urls');
 
 var CleanCSS = {
   process: function(data, options) {
-    var context = {
-      specialComments: [],
-      freeTextBlocks: [],
-      urlBlocks: []
-    };
     var replace = function() {
       if (typeof arguments[0] == 'function')
         arguments[0]();
@@ -28,14 +29,6 @@ var CleanCSS = {
     this.lineBreak = lineBreak;
 
     options = options || {};
-
-    // * - leave all important comments
-    // 1 - leave first important comment only
-    // 0 - strip all important comments
-    options.keepSpecialComments = 'keepSpecialComments' in options ?
-      options.keepSpecialComments :
-      '*';
-
     options.keepBreaks = options.keepBreaks || false;
 
     //active by default
@@ -59,9 +52,18 @@ var CleanCSS = {
       };
     }
 
+    var commentsProcessor = new CommentsProcessor(
+      'keepSpecialComments' in options ? options.keepSpecialComments : '*',
+      options.keepBreaks,
+      lineBreak
+    );
+    var freeTextProcessor = new FreeTextProcessor();
+    var urlsProcessor = new UrlsProcessor();
+    var importInliner = new ImportInliner();
+
     var removeComments = function() {
-      replace(function stripComments() {
-        data = CleanCSS._stripComments(context, data);
+      replace(function escapeComments() {
+        data = commentsProcessor.escape(data);
       });
     };
 
@@ -73,7 +75,7 @@ var CleanCSS = {
     if (options.processImport) {
       // inline all imports
       replace(function inlineImports() {
-        data = CleanCSS._inlineImports(data, {
+        data = importInliner.process(data, {
           root: options.root || process.cwd(),
           relativeTo: options.relativeTo
         });
@@ -124,14 +126,12 @@ var CleanCSS = {
         return match;
     });
 
-    // replace all free text content with a placeholder
-    replace(function stripFreeText() {
-      data = CleanCSS._stripFreeText(context, data);
+    replace(function escapeFreeText() {
+      data = freeTextProcessor.escape(data);
     });
 
-    // replace url(...) with a placeholder
-    replace(function stripUrls() {
-      data = CleanCSS._stripUrls(context, data);
+    replace(function escapeUrls() {
+      data = urlsProcessor.escape(data);
     });
 
     // line breaks
@@ -166,47 +166,20 @@ var CleanCSS = {
     // trailing semicolons
     replace(/;\}/g, '}');
 
-    // hsl to hex colors
-    replace(/hsl\((\d+),(\d+)%?,(\d+)%?\)/g, function(match, hue, saturation, lightness) {
-      var asRgb = CleanCSS._hslToRgb(hue, saturation, lightness);
-      var redAsHex = asRgb[0].toString(16);
-      var greenAsHex = asRgb[1].toString(16);
-      var blueAsHex = asRgb[2].toString(16);
-
-      return '#' +
-        ((redAsHex.length == 1 ? '0' : '') + redAsHex) +
-        ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) +
-        ((blueAsHex.length == 1 ? '0' : '') + blueAsHex);
+    replace(function hsl2Hex() {
+      data = new ColorHSLToHex(data).process();
     });
 
-    // rgb to hex colors
-    replace(/rgb\((\d+),(\d+),(\d+)\)/g, function(match, red, green, blue) {
-      var redAsHex = parseInt(red, 10).toString(16);
-      var greenAsHex = parseInt(green, 10).toString(16);
-      var blueAsHex = parseInt(blue, 10).toString(16);
-
-      return '#' +
-        ((redAsHex.length == 1 ? '0' : '') + redAsHex) +
-        ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) +
-        ((blueAsHex.length == 1 ? '0' : '') + blueAsHex);
+    replace(function rgb2Hex() {
+      data = new ColorRGBToHex(data).process();
     });
 
-    // long hex to short hex colors
-    replace(/([,: \(])#([0-9a-f]{6})/gi, function(match, prefix, color) {
-      if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5])
-        return prefix + '#' + color[0] + color[2] + color[4];
-      else
-        return prefix + '#' + color;
+    replace(function longToShortHex() {
+      data = new ColorLongToShortHex(data).process();
     });
 
-    // replace color name with hex values if shorter (or the other way around)
-    ['toHex', 'toName'].forEach(function(type) {
-      var pattern = '(' + Object.keys(colorShortening[type]).join('|') + ')';
-      var colorSwitcher = function(match, prefix, colorValue, suffix) {
-        return prefix + colorShortening[type][colorValue.toLowerCase()] + suffix;
-      };
-      replace(new RegExp('([ :,\\(])' + pattern + '([;\\}!\\) ])', 'ig'), colorSwitcher);
-      replace(new RegExp('(,)' + pattern + '(,)', 'ig'), colorSwitcher);
+    replace(function shortenColors() {
+      data = new ColorShortener(data).process();
     });
 
     // replace font weight with numerical value
@@ -249,43 +222,8 @@ var CleanCSS = {
     replace(/:0 0 0 0([^\.])/g, ':0$1');
     replace(/([: ,=\-])0\.(\d)/g, '$1.$2');
 
-    // shorthand notations
-    var shorthandRegex = function(repeats, hasSuffix) {
-      var pattern = '(padding|margin|border\\-width|border\\-color|border\\-style|border\\-radius):';
-      for (var i = 0; i < repeats; i++) {
-        pattern += '([\\d\\w\\.%#\\(\\),]+)' + (i < repeats - 1 ? ' ' : '');
-      }
-      return new RegExp(pattern + (hasSuffix ? '([;}])' : ''), 'g');
-    };
-
-    // 4 size values into less
-    replace(shorthandRegex(4), function(match, property, size1, size2, size3, size4) {
-      if (size1 === size2 && size1 === size3 && size1 === size4)
-        return property + ':' + size1;
-      else if (size1 === size3 && size2 === size4)
-        return property + ':' + size1 + ' ' + size2;
-      else if (size2 === size4)
-        return property + ':' + size1 + ' ' + size2 + ' ' + size3;
-      else
-        return match;
-    });
-
-    // 3 size values into less
-    replace(shorthandRegex(3, true), function(match, property, size1, size2, size3, suffix) {
-      if (size1 === size2 && size1 === size3)
-        return property + ':' + size1 + suffix;
-      else if (size1 === size3)
-        return property + ':' + size1 + ' ' + size2 + suffix;
-      else
-        return match;
-    });
-
-    // same 2 values into one
-    replace(shorthandRegex(2, true), function(match, property, size1, size2, suffix) {
-      if (size1 === size2)
-        return property + ':' + size1 + suffix;
-      else
-        return match;
+    replace(function shorthandNotations() {
+      data = new ShorthandNotations(data).process();
     });
 
     // restore rect(...) zeros syntax for 4 zeros
@@ -299,28 +237,14 @@ var CleanCSS = {
       return match.replace(/\+/g, ' + ');
     });
 
-    // Restore urls, content content, and special comments (in that order)
-    replace(/__URL__/g, function() {
-      return context.urlBlocks.shift();
+    replace(function restoreUrls() {
+      data = urlsProcessor.restore(data);
     });
-
-    replace(/__CSSFREETEXT__/g, function() {
-      return context.freeTextBlocks.shift();
+    replace(function restoreFreeText() {
+      data = freeTextProcessor.restore(data);
     });
-
-    var specialCommentsCount = context.specialComments.length;
-    var breakSuffix = options.keepBreaks ? lineBreak : '';
-    replace(new RegExp('__CSSCOMMENT__(' + lineBreak + '| )?', 'g'), function() {
-      switch (options.keepSpecialComments) {
-        case '*':
-          return context.specialComments.shift() + breakSuffix;
-        case 1:
-          return context.specialComments.length == specialCommentsCount ?
-            context.specialComments.shift() + breakSuffix :
-            '';
-        case 0:
-          return '';
-      }
+    replace(function restoreComments() {
+      data = commentsProcessor.restore(data);
     });
 
     // move first charset to the beginning
@@ -347,234 +271,6 @@ var CleanCSS = {
 
     // trim spaces at beginning and end
     return data.trim();
-  },
-
-  // Inlines all imports taking care of repetitions, unknown files, and cilcular dependencies
-  _inlineImports: function(data, options) {
-    var tempData = [];
-    var nextStart = 0;
-    var nextEnd = 0;
-    var cursor = 0;
-
-    options.relativeTo = options.relativeTo || options.root;
-    options._baseRelativeTo = options._baseRelativeTo || options.relativeTo;
-    options.visited = options.visited || [];
-
-    var inlinedFile = function() {
-      var importedFile = data
-        .substring(data.indexOf(' ', nextStart) + 1, nextEnd)
-        .replace(/^url\(/, '')
-        .replace(/\)$/, '')
-        .replace(/['"]/g, '');
-
-      if (/^(http|https):\/\//.test(importedFile) || /^\/\//.test(importedFile))
-        return '@import url(' + importedFile + ');';
-
-      var relativeTo = importedFile[0] == '/' ?
-        options.root :
-        options.relativeTo;
-
-      var fullPath = path.resolve(path.join(relativeTo, importedFile));
-
-      if (existsSync(fullPath) && fs.statSync(fullPath).isFile() && options.visited.indexOf(fullPath) == -1) {
-        options.visited.push(fullPath);
-
-        var importedData = fs.readFileSync(fullPath, 'utf8');
-        var importRelativeTo = path.dirname(fullPath);
-        importedData = CleanCSS._rebaseRelativeURLs(importedData, importRelativeTo, options._baseRelativeTo);
-        return CleanCSS._inlineImports(importedData, {
-          root: options.root,
-          relativeTo: importRelativeTo,
-          _baseRelativeTo: options.baseRelativeTo,
-          visited: options.visited
-        });
-      } else {
-        return '';
-      }
-    };
-
-    for (; nextEnd < data.length; ) {
-      nextStart = data.indexOf('@import', cursor);
-      if (nextStart == -1)
-        break;
-
-      nextEnd = data.indexOf(';', nextStart);
-      if (nextEnd == -1)
-        break;
-
-      tempData.push(data.substring(cursor, nextStart));
-      tempData.push(inlinedFile());
-      cursor = nextEnd + 1;
-    }
-
-    return tempData.length > 0 ?
-      tempData.join('') + data.substring(cursor, data.length) :
-      data;
-  },
-
-  _rebaseRelativeURLs: function(data, fromBase, toBase) {
-    var tempData = [];
-    var nextStart = 0;
-    var nextEnd = 0;
-    var cursor = 0;
-
-    for (; nextEnd < data.length; ) {
-      nextStart = data.indexOf('url(', nextEnd);
-      if (nextStart == -1)
-        break;
-      nextEnd = data.indexOf(')', nextStart + 4);
-      if (nextEnd == -1)
-        break;
-
-      tempData.push(data.substring(cursor, nextStart));
-      var url = data.substring(nextStart + 4, nextEnd).replace(/['"]/g, '');
-      if (url[0] != '/' && url.indexOf('data:') !== 0 && url.substring(url.length - 4) != '.css') {
-        url = path.relative(toBase, path.join(fromBase, url)).replace(/\\/g, '/');
-      }
-      tempData.push('url(' + url + ')');
-      cursor = nextEnd + 1;
-    }
-
-    return tempData.length > 0 ?
-      tempData.join('') + data.substring(cursor, data.length) :
-      data;
-  },
-
-  // Strip special comments (/*! ... */) by replacing them by __CSSCOMMENT__ marker
-  // for further restoring. Plain comments are removed. It's done by scanning datq using
-  // String#indexOf scanning instead of regexps to speed up the process.
-  _stripComments: function(context, data) {
-    var tempData = [];
-    var nextStart = 0;
-    var nextEnd = 0;
-    var cursor = 0;
-
-    for (; nextEnd < data.length; ) {
-      nextStart = data.indexOf('/*', nextEnd);
-      nextEnd = data.indexOf('*/', nextStart + 2);
-      if (nextStart == -1 || nextEnd == -1)
-        break;
-
-      tempData.push(data.substring(cursor, nextStart));
-      if (data[nextStart + 2] == '!') {
-        // in case of special comments, replace them with a placeholder
-        context.specialComments.push(data.substring(nextStart, nextEnd + 2));
-        tempData.push('__CSSCOMMENT__');
-      }
-      cursor = nextEnd + 2;
-    }
-
-    return tempData.length > 0 ?
-      tempData.join('') + data.substring(cursor, data.length) :
-      data;
-  },
-
-  // Strip content tags by replacing them by the __CSSFREETEXT__
-  // marker for further restoring. It's done via string scanning
-  // instead of regexps to speed up the process.
-  _stripFreeText: function(context, data) {
-    var tempData = [];
-    var nextStart = 0;
-    var nextEnd = 0;
-    var cursor = 0;
-    var matchedParenthesis = null;
-    var singleParenthesis = "'";
-    var doubleParenthesis = '"';
-    var dataLength = data.length;
-
-    for (; nextEnd < data.length; ) {
-      var nextStartSingle = data.indexOf(singleParenthesis, nextEnd + 1);
-      var nextStartDouble = data.indexOf(doubleParenthesis, nextEnd + 1);
-
-      if (nextStartSingle == -1)
-        nextStartSingle = dataLength;
-      if (nextStartDouble == -1)
-        nextStartDouble = dataLength;
-
-      if (nextStartSingle < nextStartDouble) {
-        nextStart = nextStartSingle;
-        matchedParenthesis = singleParenthesis;
-      } else {
-        nextStart = nextStartDouble;
-        matchedParenthesis = doubleParenthesis;
-      }
-
-      if (nextStart == -1)
-        break;
-
-      nextEnd = data.indexOf(matchedParenthesis, nextStart + 1);
-      if (nextStart == -1 || nextEnd == -1)
-        break;
-
-      tempData.push(data.substring(cursor, nextStart));
-      tempData.push('__CSSFREETEXT__');
-      context.freeTextBlocks.push(data.substring(nextStart, nextEnd + 1));
-      cursor = nextEnd + 1;
-    }
-
-    return tempData.length > 0 ?
-      tempData.join('') + data.substring(cursor, data.length) :
-      data;
-  },
-
-  // Strip urls by replacing them by the __URL__
-  // marker for further restoring. It's done via string scanning
-  // instead of regexps to speed up the process.
-  _stripUrls: function(context, data) {
-    var nextStart = 0;
-    var nextEnd = 0;
-    var cursor = 0;
-    var tempData = [];
-
-    for (; nextEnd < data.length; ) {
-      nextStart = data.indexOf('url(', nextEnd);
-      if (nextStart == -1)
-        break;
-
-      nextEnd = data.indexOf(')', nextStart);
-
-      tempData.push(data.substring(cursor, nextStart));
-      tempData.push('__URL__');
-      context.urlBlocks.push(data.substring(nextStart, nextEnd + 1));
-      cursor = nextEnd + 1;
-    }
-
-    return tempData.length > 0 ?
-      tempData.join('') + data.substring(cursor, data.length) :
-      data;
-  },
-
-  // HSL to RGB converter. Both methods taken and adapted from:
-  // http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
-  _hslToRgb: function(h, s, l) {
-    var r, g, b;
-
-    h = ~~h / 360;
-    s = ~~s / 100;
-    l = ~~l / 100;
-
-    if (s === 0) {
-      r = g = b = l; // achromatic
-    } else {
-      var q = l < 0.5 ?
-        l * (1 + s) :
-        l + s - l * s;
-      var p = 2 * l - q;
-      r = this._hueToRgb(p, q, h + 1/3);
-      g = this._hueToRgb(p, q, h);
-      b = this._hueToRgb(p, q, h - 1/3);
-    }
-
-    return [~~(r * 255), ~~(g * 255), ~~(b * 255)];
-  },
-
-  _hueToRgb: function(p, q, t) {
-    if (t < 0) t += 1;
-    if (t > 1) t -= 1;
-    if (t < 1/6) return p + (q - p) * 6 * t;
-    if (t < 1/2) return q;
-    if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
-    return p;
   }
 };
 
diff --git a/lib/color-shortening.js b/lib/color-shortening.js
deleted file mode 100644 (file)
index 3603d4e..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-var COLORS = {
-  aliceblue: '#f0f8ff',
-  antiquewhite: '#faebd7',
-  aqua: '#0ff',
-  aquamarine: '#7fffd4',
-  azure: '#f0ffff',
-  beige: '#f5f5dc',
-  bisque: '#ffe4c4',
-  black: '#000',
-  blanchedalmond: '#ffebcd',
-  blue: '#00f',
-  blueviolet: '#8a2be2',
-  brown: '#a52a2a',
-  burlywood: '#deb887',
-  cadetblue: '#5f9ea0',
-  chartreuse: '#7fff00',
-  chocolate: '#d2691e',
-  coral: '#ff7f50',
-  cornflowerblue: '#6495ed',
-  cornsilk: '#fff8dc',
-  crimson: '#dc143c',
-  cyan: '#0ff',
-  darkblue: '#00008b',
-  darkcyan: '#008b8b',
-  darkgoldenrod: '#b8860b',
-  darkgray: '#a9a9a9',
-  darkgreen: '#006400',
-  darkkhaki: '#bdb76b',
-  darkmagenta: '#8b008b',
-  darkolivegreen: '#556b2f',
-  darkorange: '#ff8c00',
-  darkorchid: '#9932cc',
-  darkred: '#8b0000',
-  darksalmon: '#e9967a',
-  darkseagreen: '#8fbc8f',
-  darkslateblue: '#483d8b',
-  darkslategray: '#2f4f4f',
-  darkturquoise: '#00ced1',
-  darkviolet: '#9400d3',
-  deeppink: '#ff1493',
-  deepskyblue: '#00bfff',
-  dimgray: '#696969',
-  dodgerblue: '#1e90ff',
-  firebrick: '#b22222',
-  floralwhite: '#fffaf0',
-  forestgreen: '#228b22',
-  fuchsia: '#f0f',
-  gainsboro: '#dcdcdc',
-  ghostwhite: '#f8f8ff',
-  gold: '#ffd700',
-  goldenrod: '#daa520',
-  gray: '#808080',
-  green: '#008000',
-  greenyellow: '#adff2f',
-  honeydew: '#f0fff0',
-  hotpink: '#ff69b4',
-  indianred: '#cd5c5c',
-  indigo: '#4b0082',
-  ivory: '#fffff0',
-  khaki: '#f0e68c',
-  lavender: '#e6e6fa',
-  lavenderblush: '#fff0f5',
-  lawngreen: '#7cfc00',
-  lemonchiffon: '#fffacd',
-  lightblue: '#add8e6',
-  lightcoral: '#f08080',
-  lightcyan: '#e0ffff',
-  lightgoldenrodyellow: '#fafad2',
-  lightgray: '#d3d3d3',
-  lightgreen: '#90ee90',
-  lightpink: '#ffb6c1',
-  lightsalmon: '#ffa07a',
-  lightseagreen: '#20b2aa',
-  lightskyblue: '#87cefa',
-  lightslategray: '#778899',
-  lightsteelblue: '#b0c4de',
-  lightyellow: '#ffffe0',
-  lime: '#0f0',
-  limegreen: '#32cd32',
-  linen: '#faf0e6',
-  magenta: '#ff00ff',
-  maroon: '#800000',
-  mediumaquamarine: '#66cdaa',
-  mediumblue: '#0000cd',
-  mediumorchid: '#ba55d3',
-  mediumpurple: '#9370db',
-  mediumseagreen: '#3cb371',
-  mediumslateblue: '#7b68ee',
-  mediumspringgreen: '#00fa9a',
-  mediumturquoise: '#48d1cc',
-  mediumvioletred: '#c71585',
-  midnightblue: '#191970',
-  mintcream: '#f5fffa',
-  mistyrose: '#ffe4e1',
-  moccasin: '#ffe4b5',
-  navajowhite: '#ffdead',
-  navy: '#000080',
-  oldlace: '#fdf5e6',
-  olive: '#808000',
-  olivedrab: '#6b8e23',
-  orange: '#ffa500',
-  orangered: '#ff4500',
-  orchid: '#da70d6',
-  palegoldenrod: '#eee8aa',
-  palegreen: '#98fb98',
-  paleturquoise: '#afeeee',
-  palevioletred: '#db7093',
-  papayawhip: '#ffefd5',
-  peachpuff: '#ffdab9',
-  peru: '#cd853f',
-  pink: '#ffc0cb',
-  plum: '#dda0dd',
-  powderblue: '#b0e0e6',
-  purple: '#800080',
-  red: '#f00',
-  rosybrown: '#bc8f8f',
-  royalblue: '#4169e1',
-  saddlebrown: '#8b4513',
-  salmon: '#fa8072',
-  sandybrown: '#f4a460',
-  seagreen: '#2e8b57',
-  seashell: '#fff5ee',
-  sienna: '#a0522d',
-  silver: '#c0c0c0',
-  skyblue: '#87ceeb',
-  slateblue: '#6a5acd',
-  slategray: '#708090',
-  snow: '#fffafa',
-  springgreen: '#00ff7f',
-  steelblue: '#4682b4',
-  tan: '#d2b48c',
-  teal: '#008080',
-  thistle: '#d8bfd8',
-  tomato: '#ff6347',
-  turquoise: '#40e0d0',
-  violet: '#ee82ee',
-  wheat: '#f5deb3',
-  white: '#fff',
-  whitesmoke: '#f5f5f5',
-  yellow: '#ff0',
-  yellowgreen: '#9acd32'
-};
-
-var toHex = {};
-var toName = {};
-
-for (var name in COLORS) {
-  var color = COLORS[name];
-  if (name.length < color.length)
-    toName[color] = name;
-  else
-    toHex[name] = color;
-}
-
-module.exports = {
-  toHex: toHex,
-  toName: toName
-};
diff --git a/lib/colors/hsl-to-hex.js b/lib/colors/hsl-to-hex.js
new file mode 100644 (file)
index 0000000..09a676a
--- /dev/null
@@ -0,0 +1,50 @@
+module.exports = function HSLToHex(data) {
+  // HSL to RGB converter. Both methods adapted from:
+  // http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+  var hslToRgb = function(h, s, l) {
+    var r, g, b;
+
+    h = ~~h / 360;
+    s = ~~s / 100;
+    l = ~~l / 100;
+
+    if (s === 0) {
+      r = g = b = l; // achromatic
+    } else {
+      var q = l < 0.5 ?
+        l * (1 + s) :
+        l + s - l * s;
+      var p = 2 * l - q;
+      r = hueToRgb(p, q, h + 1/3);
+      g = hueToRgb(p, q, h);
+      b = hueToRgb(p, q, h - 1/3);
+    }
+
+    return [~~(r * 255), ~~(g * 255), ~~(b * 255)];
+  };
+
+  var hueToRgb = function(p, q, t) {
+    if (t < 0) t += 1;
+    if (t > 1) t -= 1;
+    if (t < 1/6) return p + (q - p) * 6 * t;
+    if (t < 1/2) return q;
+    if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+    return p;
+  };
+
+  return {
+    process: function() {
+      return data.replace(/hsl\((\d+),(\d+)%?,(\d+)%?\)/g, function(match, hue, saturation, lightness) {
+        var asRgb = hslToRgb(hue, saturation, lightness);
+        var redAsHex = asRgb[0].toString(16);
+        var greenAsHex = asRgb[1].toString(16);
+        var blueAsHex = asRgb[2].toString(16);
+
+        return '#' +
+          ((redAsHex.length == 1 ? '0' : '') + redAsHex) +
+          ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) +
+          ((blueAsHex.length == 1 ? '0' : '') + blueAsHex);
+      });
+    }
+  };
+};
diff --git a/lib/colors/long-to-short-hex.js b/lib/colors/long-to-short-hex.js
new file mode 100644 (file)
index 0000000..87fa31b
--- /dev/null
@@ -0,0 +1,12 @@
+module.exports = function LongToShortHex(data) {
+  return {
+    process: function() {
+      return data.replace(/([,: \(])#([0-9a-f]{6})/gi, function(match, prefix, color) {
+        if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5])
+          return prefix + '#' + color[0] + color[2] + color[4];
+        else
+          return prefix + '#' + color;
+      });
+    }
+  };
+};
diff --git a/lib/colors/rgb-to-hex.js b/lib/colors/rgb-to-hex.js
new file mode 100644 (file)
index 0000000..d235289
--- /dev/null
@@ -0,0 +1,16 @@
+module.exports = function RGBToHex(data) {
+  return {
+    process: function() {
+      return data.replace(/rgb\((\d+),(\d+),(\d+)\)/g, function(match, red, green, blue) {
+        var redAsHex = parseInt(red, 10).toString(16);
+        var greenAsHex = parseInt(green, 10).toString(16);
+        var blueAsHex = parseInt(blue, 10).toString(16);
+
+        return '#' +
+          ((redAsHex.length == 1 ? '0' : '') + redAsHex) +
+          ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) +
+          ((blueAsHex.length == 1 ? '0' : '') + blueAsHex);
+      });
+    }
+  };
+};
diff --git a/lib/colors/shortener.js b/lib/colors/shortener.js
new file mode 100644 (file)
index 0000000..7476f09
--- /dev/null
@@ -0,0 +1,174 @@
+module.exports = function Shortener(data) {
+  var COLORS = {
+    aliceblue: '#f0f8ff',
+    antiquewhite: '#faebd7',
+    aqua: '#0ff',
+    aquamarine: '#7fffd4',
+    azure: '#f0ffff',
+    beige: '#f5f5dc',
+    bisque: '#ffe4c4',
+    black: '#000',
+    blanchedalmond: '#ffebcd',
+    blue: '#00f',
+    blueviolet: '#8a2be2',
+    brown: '#a52a2a',
+    burlywood: '#deb887',
+    cadetblue: '#5f9ea0',
+    chartreuse: '#7fff00',
+    chocolate: '#d2691e',
+    coral: '#ff7f50',
+    cornflowerblue: '#6495ed',
+    cornsilk: '#fff8dc',
+    crimson: '#dc143c',
+    cyan: '#0ff',
+    darkblue: '#00008b',
+    darkcyan: '#008b8b',
+    darkgoldenrod: '#b8860b',
+    darkgray: '#a9a9a9',
+    darkgreen: '#006400',
+    darkkhaki: '#bdb76b',
+    darkmagenta: '#8b008b',
+    darkolivegreen: '#556b2f',
+    darkorange: '#ff8c00',
+    darkorchid: '#9932cc',
+    darkred: '#8b0000',
+    darksalmon: '#e9967a',
+    darkseagreen: '#8fbc8f',
+    darkslateblue: '#483d8b',
+    darkslategray: '#2f4f4f',
+    darkturquoise: '#00ced1',
+    darkviolet: '#9400d3',
+    deeppink: '#ff1493',
+    deepskyblue: '#00bfff',
+    dimgray: '#696969',
+    dodgerblue: '#1e90ff',
+    firebrick: '#b22222',
+    floralwhite: '#fffaf0',
+    forestgreen: '#228b22',
+    fuchsia: '#f0f',
+    gainsboro: '#dcdcdc',
+    ghostwhite: '#f8f8ff',
+    gold: '#ffd700',
+    goldenrod: '#daa520',
+    gray: '#808080',
+    green: '#008000',
+    greenyellow: '#adff2f',
+    honeydew: '#f0fff0',
+    hotpink: '#ff69b4',
+    indianred: '#cd5c5c',
+    indigo: '#4b0082',
+    ivory: '#fffff0',
+    khaki: '#f0e68c',
+    lavender: '#e6e6fa',
+    lavenderblush: '#fff0f5',
+    lawngreen: '#7cfc00',
+    lemonchiffon: '#fffacd',
+    lightblue: '#add8e6',
+    lightcoral: '#f08080',
+    lightcyan: '#e0ffff',
+    lightgoldenrodyellow: '#fafad2',
+    lightgray: '#d3d3d3',
+    lightgreen: '#90ee90',
+    lightpink: '#ffb6c1',
+    lightsalmon: '#ffa07a',
+    lightseagreen: '#20b2aa',
+    lightskyblue: '#87cefa',
+    lightslategray: '#778899',
+    lightsteelblue: '#b0c4de',
+    lightyellow: '#ffffe0',
+    lime: '#0f0',
+    limegreen: '#32cd32',
+    linen: '#faf0e6',
+    magenta: '#ff00ff',
+    maroon: '#800000',
+    mediumaquamarine: '#66cdaa',
+    mediumblue: '#0000cd',
+    mediumorchid: '#ba55d3',
+    mediumpurple: '#9370db',
+    mediumseagreen: '#3cb371',
+    mediumslateblue: '#7b68ee',
+    mediumspringgreen: '#00fa9a',
+    mediumturquoise: '#48d1cc',
+    mediumvioletred: '#c71585',
+    midnightblue: '#191970',
+    mintcream: '#f5fffa',
+    mistyrose: '#ffe4e1',
+    moccasin: '#ffe4b5',
+    navajowhite: '#ffdead',
+    navy: '#000080',
+    oldlace: '#fdf5e6',
+    olive: '#808000',
+    olivedrab: '#6b8e23',
+    orange: '#ffa500',
+    orangered: '#ff4500',
+    orchid: '#da70d6',
+    palegoldenrod: '#eee8aa',
+    palegreen: '#98fb98',
+    paleturquoise: '#afeeee',
+    palevioletred: '#db7093',
+    papayawhip: '#ffefd5',
+    peachpuff: '#ffdab9',
+    peru: '#cd853f',
+    pink: '#ffc0cb',
+    plum: '#dda0dd',
+    powderblue: '#b0e0e6',
+    purple: '#800080',
+    red: '#f00',
+    rosybrown: '#bc8f8f',
+    royalblue: '#4169e1',
+    saddlebrown: '#8b4513',
+    salmon: '#fa8072',
+    sandybrown: '#f4a460',
+    seagreen: '#2e8b57',
+    seashell: '#fff5ee',
+    sienna: '#a0522d',
+    silver: '#c0c0c0',
+    skyblue: '#87ceeb',
+    slateblue: '#6a5acd',
+    slategray: '#708090',
+    snow: '#fffafa',
+    springgreen: '#00ff7f',
+    steelblue: '#4682b4',
+    tan: '#d2b48c',
+    teal: '#008080',
+    thistle: '#d8bfd8',
+    tomato: '#ff6347',
+    turquoise: '#40e0d0',
+    violet: '#ee82ee',
+    wheat: '#f5deb3',
+    white: '#fff',
+    whitesmoke: '#f5f5f5',
+    yellow: '#ff0',
+    yellowgreen: '#9acd32'
+  };
+
+  var toHex = {};
+  var toName = {};
+
+  for (var name in COLORS) {
+    var color = COLORS[name];
+    if (name.length < color.length)
+      toName[color] = name;
+    else
+      toHex[name] = color;
+  }
+
+  return {
+    toHex: toHex,
+    toName: toName,
+
+    // replace color name with hex values if shorter (or the other way around)
+    process: function() {
+      [toHex, toName].forEach(function(conversion) {
+        var pattern = '(' + Object.keys(conversion).join('|') + ')';
+        var colorSwitcher = function(match, prefix, colorValue, suffix) {
+          return prefix + conversion[colorValue.toLowerCase()] + suffix;
+        };
+        data = data.replace(new RegExp('([ :,\\(])' + pattern + '([;\\}!\\) ])', 'ig'), colorSwitcher);
+        data = data.replace(new RegExp('(,)' + pattern + '(,)', 'ig'), colorSwitcher);
+      });
+
+      return data;
+    }
+  };
+};
diff --git a/lib/imports/inliner.js b/lib/imports/inliner.js
new file mode 100644 (file)
index 0000000..9b73345
--- /dev/null
@@ -0,0 +1,100 @@
+var fs = require('fs');
+var path = require('path');
+var existsSync = fs.existsSync || path.existsSync;
+
+module.exports = function Inliner() {
+  var process = function(data, options) {
+    var tempData = [];
+    var nextStart = 0;
+    var nextEnd = 0;
+    var cursor = 0;
+
+    options.relativeTo = options.relativeTo || options.root;
+    options._baseRelativeTo = options._baseRelativeTo || options.relativeTo;
+    options.visited = options.visited || [];
+
+    for (; nextEnd < data.length; ) {
+      nextStart = data.indexOf('@import', cursor);
+      if (nextStart == -1)
+        break;
+
+      nextEnd = data.indexOf(';', nextStart);
+      if (nextEnd == -1)
+        break;
+
+      tempData.push(data.substring(cursor, nextStart));
+      tempData.push(inlinedFile(data, nextStart, nextEnd, options));
+      cursor = nextEnd + 1;
+    }
+
+    return tempData.length > 0 ?
+      tempData.join('') + data.substring(cursor, data.length) :
+      data;
+  };
+
+  var inlinedFile = function(data, nextStart, nextEnd, options) {
+    var importedFile = data
+      .substring(data.indexOf(' ', nextStart) + 1, nextEnd)
+      .replace(/^url\(/, '')
+      .replace(/\)$/, '')
+      .replace(/['"]/g, '');
+
+    if (/^(http|https):\/\//.test(importedFile) || /^\/\//.test(importedFile))
+      return '@import url(' + importedFile + ');';
+
+    var relativeTo = importedFile[0] == '/' ?
+      options.root :
+      options.relativeTo;
+
+    var fullPath = path.resolve(path.join(relativeTo, importedFile));
+
+    if (existsSync(fullPath) && fs.statSync(fullPath).isFile() && options.visited.indexOf(fullPath) == -1) {
+      options.visited.push(fullPath);
+
+      var importedData = fs.readFileSync(fullPath, 'utf8');
+      var importRelativeTo = path.dirname(fullPath);
+      importedData = rebaseRelativeURLs(importedData, importRelativeTo, options._baseRelativeTo);
+      return process(importedData, {
+        root: options.root,
+        relativeTo: importRelativeTo,
+        _baseRelativeTo: options.baseRelativeTo,
+        visited: options.visited
+      });
+    } else {
+      return '';
+    }
+  };
+
+  var rebaseRelativeURLs = function(data, fromBase, toBase) {
+    var tempData = [];
+    var nextStart = 0;
+    var nextEnd = 0;
+    var cursor = 0;
+
+    for (; nextEnd < data.length; ) {
+      nextStart = data.indexOf('url(', nextEnd);
+      if (nextStart == -1)
+        break;
+      nextEnd = data.indexOf(')', nextStart + 4);
+      if (nextEnd == -1)
+        break;
+
+      tempData.push(data.substring(cursor, nextStart));
+      var url = data.substring(nextStart + 4, nextEnd).replace(/['"]/g, '');
+      if (url[0] != '/' && url.indexOf('data:') !== 0 && url.substring(url.length - 4) != '.css') {
+        url = path.relative(toBase, path.join(fromBase, url)).replace(/\\/g, '/');
+      }
+      tempData.push('url(' + url + ')');
+      cursor = nextEnd + 1;
+    }
+
+    return tempData.length > 0 ?
+      tempData.join('') + data.substring(cursor, data.length) :
+      data;
+  };
+
+  return {
+    // Inlines all imports taking care of repetitions, unknown files, and circular dependencies
+    process: process
+  };
+};
diff --git a/lib/properties/shorthand-notations.js b/lib/properties/shorthand-notations.js
new file mode 100644 (file)
index 0000000..8dcf363
--- /dev/null
@@ -0,0 +1,51 @@
+module.exports = function ShorthandNotations(data) {
+  // shorthand notations
+  var shorthandRegex = function(repeats, hasSuffix) {
+    var pattern = '(padding|margin|border\\-width|border\\-color|border\\-style|border\\-radius):';
+    for (var i = 0; i < repeats; i++) {
+      pattern += '([\\d\\w\\.%#\\(\\),]+)' + (i < repeats - 1 ? ' ' : '');
+    }
+    return new RegExp(pattern + (hasSuffix ? '([;}])' : ''), 'g');
+  };
+
+  var from4Values = function() {
+    return data.replace(shorthandRegex(4), function(match, property, size1, size2, size3, size4) {
+      if (size1 === size2 && size1 === size3 && size1 === size4)
+        return property + ':' + size1;
+      else if (size1 === size3 && size2 === size4)
+        return property + ':' + size1 + ' ' + size2;
+      else if (size2 === size4)
+        return property + ':' + size1 + ' ' + size2 + ' ' + size3;
+      else
+        return match;
+    });
+  };
+
+  var from3Values = function() {
+    return data.replace(shorthandRegex(3, true), function(match, property, size1, size2, size3, suffix) {
+      if (size1 === size2 && size1 === size3)
+        return property + ':' + size1 + suffix;
+      else if (size1 === size3)
+        return property + ':' + size1 + ' ' + size2 + suffix;
+      else
+        return match;
+    });
+  };
+
+  var from2Values = function() {
+    return data.replace(shorthandRegex(2, true), function(match, property, size1, size2, suffix) {
+      if (size1 === size2)
+        return property + ':' + size1 + suffix;
+      else
+        return match;
+    });
+  };
+
+  return {
+    process: function() {
+      data = from4Values();
+      data = from3Values();
+      return from2Values();
+    }
+  };
+};
diff --git a/lib/text/comments.js b/lib/text/comments.js
new file mode 100644 (file)
index 0000000..644e71d
--- /dev/null
@@ -0,0 +1,52 @@
+module.exports = function Comments(keepSpecialComments, keepBreaks, lineBreak) {
+  var comments = [];
+
+  return {
+    // Strip special comments (/*! ... */) by replacing them by __CSSCOMMENT__ marker
+    // for further restoring. Plain comments are removed. It's done by scanning datq using
+    // String#indexOf scanning instead of regexps to speed up the process.
+    escape: function(data) {
+      var tempData = [];
+      var nextStart = 0;
+      var nextEnd = 0;
+      var cursor = 0;
+
+      for (; nextEnd < data.length; ) {
+        nextStart = data.indexOf('/*', nextEnd);
+        nextEnd = data.indexOf('*/', nextStart + 2);
+        if (nextStart == -1 || nextEnd == -1)
+          break;
+
+        tempData.push(data.substring(cursor, nextStart));
+        if (data[nextStart + 2] == '!') {
+          // in case of special comments, replace them with a placeholder
+          comments.push(data.substring(nextStart, nextEnd + 2));
+          tempData.push('__CSSCOMMENT__');
+        }
+        cursor = nextEnd + 2;
+      }
+
+      return tempData.length > 0 ?
+        tempData.join('') + data.substring(cursor, data.length) :
+        data;
+    },
+
+    restore: function(data) {
+      var commentsCount = comments.length;
+      var breakSuffix = keepBreaks ? lineBreak : '';
+
+      return data.replace(new RegExp('__CSSCOMMENT__(' + lineBreak + '| )?', 'g'), function() {
+        switch (keepSpecialComments) {
+          case '*':
+            return comments.shift() + breakSuffix;
+          case 1:
+            return comments.length == commentsCount ?
+              comments.shift() + breakSuffix :
+              '';
+          case 0:
+            return '';
+        }
+      });
+    }
+  };
+};
diff --git a/lib/text/free.js b/lib/text/free.js
new file mode 100644 (file)
index 0000000..973e326
--- /dev/null
@@ -0,0 +1,59 @@
+module.exports = function Free() {
+  var texts = [];
+
+  return {
+    // Strip content tags by replacing them by the __CSSFREETEXT__
+    // marker for further restoring. It's done via string scanning
+    // instead of regexps to speed up the process.
+    escape: function(data) {
+      var tempData = [];
+      var nextStart = 0;
+      var nextEnd = 0;
+      var cursor = 0;
+      var matchedParenthesis = null;
+      var singleParenthesis = "'";
+      var doubleParenthesis = '"';
+      var dataLength = data.length;
+
+      for (; nextEnd < data.length; ) {
+        var nextStartSingle = data.indexOf(singleParenthesis, nextEnd + 1);
+        var nextStartDouble = data.indexOf(doubleParenthesis, nextEnd + 1);
+
+        if (nextStartSingle == -1)
+          nextStartSingle = dataLength;
+        if (nextStartDouble == -1)
+          nextStartDouble = dataLength;
+
+        if (nextStartSingle < nextStartDouble) {
+          nextStart = nextStartSingle;
+          matchedParenthesis = singleParenthesis;
+        } else {
+          nextStart = nextStartDouble;
+          matchedParenthesis = doubleParenthesis;
+        }
+
+        if (nextStart == -1)
+          break;
+
+        nextEnd = data.indexOf(matchedParenthesis, nextStart + 1);
+        if (nextStart == -1 || nextEnd == -1)
+          break;
+
+        tempData.push(data.substring(cursor, nextStart));
+        tempData.push('__CSSFREETEXT__');
+        texts.push(data.substring(nextStart, nextEnd + 1));
+        cursor = nextEnd + 1;
+      }
+
+      return tempData.length > 0 ?
+        tempData.join('') + data.substring(cursor, data.length) :
+        data;
+    },
+
+    restore: function(data) {
+      return data.replace(/__CSSFREETEXT__/g, function() {
+        return texts.shift();
+      });
+    }
+  };
+};
diff --git a/lib/text/urls.js b/lib/text/urls.js
new file mode 100644 (file)
index 0000000..7e83ee6
--- /dev/null
@@ -0,0 +1,38 @@
+module.exports = function Urls() {
+  var urls = [];
+
+  return {
+    // Strip urls by replacing them by the __URL__
+    // marker for further restoring. It's done via string scanning
+    // instead of regexps to speed up the process.
+    escape: function(data) {
+      var nextStart = 0;
+      var nextEnd = 0;
+      var cursor = 0;
+      var tempData = [];
+
+      for (; nextEnd < data.length; ) {
+        nextStart = data.indexOf('url(', nextEnd);
+        if (nextStart == -1)
+          break;
+
+        nextEnd = data.indexOf(')', nextStart);
+
+        tempData.push(data.substring(cursor, nextStart));
+        tempData.push('__URL__');
+        urls.push(data.substring(nextStart, nextEnd + 1));
+        cursor = nextEnd + 1;
+      }
+
+      return tempData.length > 0 ?
+        tempData.join('') + data.substring(cursor, data.length) :
+        data;
+    },
+
+    restore: function(data) {
+      return data.replace(/__URL__/g, function() {
+        return urls.shift();
+      });
+    }
+  };
+};
index ad49693..3be42a3 100644 (file)
@@ -2,7 +2,7 @@ var vows = require('vows');
 var assert = require('assert');
 var path = require('path');
 var cleanCSS = require('../index');
-var colorShortening = require('../lib/color-shortening');
+var ColorShortener = require('../lib/colors/shortener');
 
 var lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
 var cssContext = function(groups, options) {
@@ -33,10 +33,11 @@ var cssContext = function(groups, options) {
 
 var colorShorteningContext = function() {
   var shortenerContext = {};
+  var shortener = new ColorShortener();
 
   ['toName', 'toHex'].forEach(function(type) {
-    for (var from in colorShortening[type]) {
-      var to = colorShortening[type][from];
+    for (var from in shortener[type]) {
+      var to = shortener[type][from];
       shortenerContext['should turn ' + from + ' into ' + to] = [
         'a{color:' + from + '}',
         'a{color:' + to + '}'