Fixes #2 - resolving @import rules.
authorGoalSmashers <jakub@goalsmashers.com>
Thu, 21 Mar 2013 21:43:54 +0000 (22:43 +0100)
committerGoalSmashers <jakub@goalsmashers.com>
Thu, 21 Mar 2013 21:43:54 +0000 (22:43 +0100)
* Supporting both relative and absolute paths (via `-r` / `root` options).

16 files changed:
History.md
README.md
bin/cleancss
lib/clean.js
test/batch-test.js
test/binary-test.js
test/data/imports-min.css [new file with mode: 0644]
test/data/imports.css [new file with mode: 0644]
test/data/partials-absolute/base.css [new file with mode: 0644]
test/data/partials-absolute/base2.css [new file with mode: 0644]
test/data/partials-absolute/extra/sub.css [new file with mode: 0644]
test/data/partials/extra/four.css [new file with mode: 0644]
test/data/partials/extra/three.css [new file with mode: 0644]
test/data/partials/one.css [new file with mode: 0644]
test/data/partials/two.css [new file with mode: 0644]
test/unit-test.js

index ac79b0d..a468e27 100644 (file)
@@ -5,6 +5,7 @@
   and attributes.
 * Fixed issue [#44](https://github.com/GoalSmashers/clean-css/issues/44) - examples in --help.
 * Fixed issue [#83](https://github.com/GoalSmashers/clean-css/issues/83) - HSL to hex color conversions.
+* Fixed issue [#2](https://github.com/GoalSmashers/clean-css/issues/2) - resolving @import rules.
 
 0.10.2 / 2013-03-19
 ==================
index fe0f6d6..cc710d8 100644 (file)
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ cleancss [options] <source-file>
 * `-b`, `--keep-line-breaks` Keep line breaks
 * `--s0` Remove all special comments (i.e. `/*! special comment */`)
 * `--s1` Remove all special comments but the first one
+* `-r`, `--root [root-path]` Set a root path to which resolve absolute @import rules
 * `-o`, `--output [output-file]` Use [output-file] as output instead of stdout
 
 #### Examples:
index 56bcee4..8476cc9 100755 (executable)
@@ -20,6 +20,7 @@ commands
   .option('-b, --keep-line-breaks', 'Keep line breaks')
   .option('--s0', 'Remove all special comments (i.e. /*! special comment */)')
   .option('--s1', 'Remove all special comments but the first one')
+  .option('-r, --root [root-path]', 'Set a root path to which resolve absolute @import rules')
   .option('-o, --output [output-file]', 'Use [output-file] as output instead of stdout');
 
 commands.on('--help', function() {
@@ -54,8 +55,6 @@ if (!fromStdin && commands.args.length == 0) {
 // Now coerce commands into CleanCSS configuration...
 if (commands.output)
   options.target = commands.output;
-if (commands.args.length > 0)
-  options.source = commands.args[0];
 if (commands.removeEmpty)
   cleanOptions.removeEmpty = true;
 if (commands.keepLineBreaks)
@@ -64,6 +63,13 @@ if (commands.s1)
   cleanOptions.keepSpecialComments = 1;
 if (commands.s0)
   cleanOptions.keepSpecialComments = 0;
+if (commands.root)
+  cleanOptions.root = commands.root;
+if (commands.args.length > 0) {
+  var source = commands.args[0];
+  options.source = source;
+  cleanOptions.relativeTo = path.dirname(path.resolve(source));
+}
 
 // ... and do the magic!
 if (options.source) {
index 3288b4d..dbfec82 100644 (file)
@@ -5,6 +5,10 @@
  * Copyright (C) 2011-2013 GoalSmashers.com
  */
 
+var fs = require('fs');
+var path = require('path');
+var existsSync = fs.existsSync || path.existsSync;
+
 var CleanCSS = {
   colors: {
     toHex: {
@@ -70,6 +74,14 @@ var CleanCSS = {
       };
     }
 
+    // inline all imports
+    replace(function inlineImports() {
+      data = CleanCSS._inlineImports(data, {
+        root: options.root || process.cwd(),
+        relativeTo: options.relativeTo
+      });
+    });
+
     // strip comments one by one
     replace(function stripComments() {
       data = CleanCSS._stripComments(context, data);
@@ -336,6 +348,60 @@ var CleanCSS = {
     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.visited = options.visited || [];
+
+    var inlinedFile = function() {
+      var importedFile = data
+        .substring(data.indexOf('(', nextStart) + 1, nextEnd)
+        .replace(/['"]/g, '');
+
+      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');
+        return CleanCSS._inlineImports(importedData, {
+          root: options.root,
+          relativeTo: path.dirname(fullPath),
+          visited: options.visited
+        });
+      } else {
+        return '';
+      }
+    };
+
+    for (; nextEnd < data.length; ) {
+      nextStart = data.indexOf('@import url(', cursor);
+      if (nextStart == -1)
+        break;
+
+      nextEnd = data.indexOf(')', nextStart);
+      if (nextEnd == -1)
+        break;
+
+      tempData.push(data.substring(cursor, nextStart));
+      tempData.push(inlinedFile());
+      cursor = nextEnd + 2;
+    }
+
+    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.
index c7ecfa9..7e0de08 100644 (file)
@@ -8,8 +8,9 @@ var lineBreak = process.platform == 'win32' ? /\r\n/g : /\n/g;
 
 var batchContexts = function() {
   var context = {};
-  fs.readdirSync(path.join(__dirname, 'data')).forEach(function(filename) {
-    if (/min.css$/.exec(filename)) return;
+  var dir = path.join(__dirname, 'data');
+  fs.readdirSync(dir).forEach(function(filename) {
+    if (/min.css$/.exec(filename) || !fs.statSync(path.join(dir, filename)).isFile()) return;
     var testName = filename.split('.')[0];
 
     context[testName] = {
@@ -19,14 +20,16 @@ var batchContexts = function() {
 
         return {
           plain: fs.readFileSync(plainPath, 'utf-8'),
-          minimized: fs.readFileSync(minPath, 'utf-8')
+          minimized: fs.readFileSync(minPath, 'utf-8'),
+          root: path.dirname(plainPath)
         };
       }
     };
     context[testName]['minimizing ' + testName + '.css'] = function(data) {
       var processed = cleanCSS.process(data.plain, {
         removeEmpty: true,
-        keepBreaks: true
+        keepBreaks: true,
+        root: data.root
       });
 
       var processedTokens = processed.split(lineBreak);
index c3a3ea7..7b13e62 100644 (file)
@@ -78,6 +78,16 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
       assert.equal(stdout, "");
     }
   }),
+  'no relative to path': binaryContext('./test/data/partials-absolute/base.css', {
+    'should not be able to resolve it fully': function(error, stdout) {
+      assert.equal(stdout, ".sub{padding:0}.base{margin:0}");
+    }
+  }),
+  'relative to path': binaryContext('-r ./test/data ./test/data/partials-absolute/base.css', {
+    'should be able to resolve it': function(error, stdout) {
+      assert.equal(stdout, ".base2{border-width:0}.sub{padding:0}.base{margin:0}");
+    }
+  }),
   'from source': binaryContext('./test/data/reset.css', {
     'should minimize': function(error, stdout) {
       var minimized = fs.readFileSync('./test/data/reset-min.css', 'utf-8').replace(lineBreak, '');
diff --git a/test/data/imports-min.css b/test/data/imports-min.css
new file mode 100644 (file)
index 0000000..4dcf005
--- /dev/null
@@ -0,0 +1,5 @@
+.one{color:red}
+.three{color:#0f0}
+.four{color:#00f}
+.two{color:#fff}
+.imports{color:#000}
\ No newline at end of file
diff --git a/test/data/imports.css b/test/data/imports.css
new file mode 100644 (file)
index 0000000..41d3f80
--- /dev/null
@@ -0,0 +1,4 @@
+@import url('./partials/one.css');
+@import url("./partials/two.css");
+
+.imports { color: #000; }
\ No newline at end of file
diff --git a/test/data/partials-absolute/base.css b/test/data/partials-absolute/base.css
new file mode 100644 (file)
index 0000000..eca57c2
--- /dev/null
@@ -0,0 +1,3 @@
+@import url(./extra/sub.css);
+
+.base { margin:0px }
\ No newline at end of file
diff --git a/test/data/partials-absolute/base2.css b/test/data/partials-absolute/base2.css
new file mode 100644 (file)
index 0000000..bb45047
--- /dev/null
@@ -0,0 +1 @@
+.base2 { border-width:0px }
\ No newline at end of file
diff --git a/test/data/partials-absolute/extra/sub.css b/test/data/partials-absolute/extra/sub.css
new file mode 100644 (file)
index 0000000..ddf9baf
--- /dev/null
@@ -0,0 +1,3 @@
+@import url(/partials-absolute/base2.css);
+
+.sub { padding:0px }
\ No newline at end of file
diff --git a/test/data/partials/extra/four.css b/test/data/partials/extra/four.css
new file mode 100644 (file)
index 0000000..33661ad
--- /dev/null
@@ -0,0 +1,3 @@
+@import url('../two.css');
+
+.four { color:#00f; }
\ No newline at end of file
diff --git a/test/data/partials/extra/three.css b/test/data/partials/extra/three.css
new file mode 100644 (file)
index 0000000..abdfe66
--- /dev/null
@@ -0,0 +1 @@
+.three { color:#0f0; }
\ No newline at end of file
diff --git a/test/data/partials/one.css b/test/data/partials/one.css
new file mode 100644 (file)
index 0000000..6085949
--- /dev/null
@@ -0,0 +1 @@
+.one { color:#f00; }
diff --git a/test/data/partials/two.css b/test/data/partials/two.css
new file mode 100644 (file)
index 0000000..afa6599
--- /dev/null
@@ -0,0 +1,5 @@
+@import url('one.css');
+@import url('extra/three.css');
+@import url('./extra/four.css');
+
+.two { color:#fff; }
index 51fe9c3..7044bad 100644 (file)
@@ -1,6 +1,7 @@
-var vows = require('vows'),
-  assert = require('assert'),
-  cleanCSS = require('../index');
+var vows = require('vows');
+var assert = require('assert');
+var path = require('path');
+var cleanCSS = require('../index');
 
 var lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
 var cssContext = function(groups, options) {
@@ -709,5 +710,69 @@ title']",
     'empty #2': 'div>a{}',
     'empty #3': 'div:nth-child(2n){}',
     'empty #4': 'a{color:#fff}div{}p{line-height:2em}'
-  })
-}).export(module);
\ No newline at end of file
+  }),
+  '@import': cssContext({
+    'empty': [
+      "@import url();",
+      ""
+    ],
+    'of unknown file': [
+      "@import url('fake.css');",
+      ""
+    ],
+    'of a directory': [
+      "@import url(test/data/partials);",
+      ""
+    ],
+    'of a real file': [
+      "@import url(test/data/partials/one.css);",
+      ".one{color:red}"
+    ],
+    'of a real file twice': [
+      "@import url(test/data/partials/one.css);@import url(test/data/partials/one.css);",
+      ".one{color:red}"
+    ],
+    'of a real file with current path prefix': [
+      "@import url(./test/data/partials/one.css);",
+      ".one{color:red}"
+    ],
+    'of a real file with quoted path': [
+      "@import url('test/data/partials/one.css');",
+      ".one{color:red}"
+    ],
+    'of more files': [
+      "@import url(test/data/partials/one.css);\n\na{}\n\n@import url(test/data/partials/extra/three.css);",
+      ".one{color:red}a{}.three{color:#0f0}"
+    ],
+    'of multi-level, circular dependency file': [
+      "@import url(test/data/partials/two.css);",
+      ".one{color:red}.three{color:#0f0}.four{color:#00f}.two{color:#fff}"
+    ]
+  }),
+  '@import with absolute paths': cssContext({
+    'of an unknown file': [
+      "@import url(/fake.css);",
+      ""
+    ],
+    'of a real file': [
+      "@import url(/partials/one.css);",
+      ".one{color:red}"
+    ],
+    'of a real file with quoted paths': [
+      "@import url(\"/partials/one.css\");",
+      ".one{color:red}"
+    ],
+    'of two files with mixed paths': [
+      "@import url(/partials/one.css);a{}@import url(partials/extra/three.css);",
+      ".one{color:red}a{}.three{color:#0f0}"
+    ],
+    'of a multi-level, circular dependency file': [
+      "@import url(/partials/two.css);",
+      ".one{color:red}.three{color:#0f0}.four{color:#00f}.two{color:#fff}"
+    ],
+    'of a multi-level, circular dependency file with mixed paths': [
+      "@import url(/partials-absolute/base.css);",
+      ".base2{border-width:0}.sub{padding:0}.base{margin:0}"
+    ]
+  }, { root: path.join(process.cwd(), 'test', 'data') })
+}).export(module);