From 430961183ed0580b58643c2e73c5e702bc20f598 Mon Sep 17 00:00:00 2001 From: GoalSmashers Date: Thu, 21 Mar 2013 22:43:54 +0100 Subject: [PATCH] Fixes #2 - resolving @import rules. * Supporting both relative and absolute paths (via `-r` / `root` options). --- History.md | 1 + README.md | 1 + bin/cleancss | 10 ++- lib/clean.js | 66 ++++++++++++++++++++ test/batch-test.js | 11 ++-- test/binary-test.js | 10 +++ test/data/imports-min.css | 5 ++ test/data/imports.css | 4 ++ test/data/partials-absolute/base.css | 3 + test/data/partials-absolute/base2.css | 1 + test/data/partials-absolute/extra/sub.css | 3 + test/data/partials/extra/four.css | 3 + test/data/partials/extra/three.css | 1 + test/data/partials/one.css | 1 + test/data/partials/two.css | 5 ++ test/unit-test.js | 75 +++++++++++++++++++++-- 16 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 test/data/imports-min.css create mode 100644 test/data/imports.css create mode 100644 test/data/partials-absolute/base.css create mode 100644 test/data/partials-absolute/base2.css create mode 100644 test/data/partials-absolute/extra/sub.css create mode 100644 test/data/partials/extra/four.css create mode 100644 test/data/partials/extra/three.css create mode 100644 test/data/partials/one.css create mode 100644 test/data/partials/two.css diff --git a/History.md b/History.md index ac79b0d4..a468e274 100644 --- a/History.md +++ b/History.md @@ -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 ================== diff --git a/README.md b/README.md index fe0f6d6a..cc710d8c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ cleancss [options] * `-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: diff --git a/bin/cleancss b/bin/cleancss index 56bcee4f..8476cc90 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -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) { diff --git a/lib/clean.js b/lib/clean.js index 3288b4df..dbfec82b 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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. diff --git a/test/batch-test.js b/test/batch-test.js index c7ecfa99..7e0de08e 100644 --- a/test/batch-test.js +++ b/test/batch-test.js @@ -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); diff --git a/test/binary-test.js b/test/binary-test.js index c3a3ea70..7b13e62d 100644 --- a/test/binary-test.js +++ b/test/binary-test.js @@ -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 index 00000000..4dcf005c --- /dev/null +++ b/test/data/imports-min.css @@ -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 index 00000000..41d3f809 --- /dev/null +++ b/test/data/imports.css @@ -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 index 00000000..eca57c23 --- /dev/null +++ b/test/data/partials-absolute/base.css @@ -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 index 00000000..bb45047a --- /dev/null +++ b/test/data/partials-absolute/base2.css @@ -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 index 00000000..ddf9baf9 --- /dev/null +++ b/test/data/partials-absolute/extra/sub.css @@ -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 index 00000000..33661ad2 --- /dev/null +++ b/test/data/partials/extra/four.css @@ -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 index 00000000..abdfe668 --- /dev/null +++ b/test/data/partials/extra/three.css @@ -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 index 00000000..60859490 --- /dev/null +++ b/test/data/partials/one.css @@ -0,0 +1 @@ +.one { color:#f00; } diff --git a/test/data/partials/two.css b/test/data/partials/two.css new file mode 100644 index 00000000..afa65995 --- /dev/null +++ b/test/data/partials/two.css @@ -0,0 +1,5 @@ +@import url('one.css'); +@import url('extra/three.css'); +@import url('./extra/four.css'); + +.two { color:#fff; } diff --git a/test/unit-test.js b/test/unit-test.js index 51fe9c3a..7044bad8 100644 --- a/test/unit-test.js +++ b/test/unit-test.js @@ -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); -- 2.34.1