diff --git a/History.md b/History.md index 5700de325..f62ad26aa 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,11 @@ +[4.1.8 / 2017-09-02](https://github.com/jakubpawlowicz/clean-css/compare/v4.1.7...v4.1.8) +================== + +* Fixed issue [#959](https://github.com/jakubpawlowicz/clean-css/issues/959) - regression in shortening long hex values. +* Fixed issue [#960](https://github.com/jakubpawlowicz/clean-css/issues/960) - better explanation of `efficiency` stat. +* Fixed issue [#965](https://github.com/jakubpawlowicz/clean-css/issues/965) - edge case in parsing comment endings. +* Fixed issue [#966](https://github.com/jakubpawlowicz/clean-css/issues/966) - remote `@import`s referenced from local ones. + [4.1.7 / 2017-07-14](https://github.com/jakubpawlowicz/clean-css/compare/v4.1.6...v4.1.7) ================== diff --git a/README.md b/README.md index b240f4381..29cb7ffce 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ console.log(output.warnings); // a list of warnings raised console.log(output.stats.originalSize); // original content size after import inlining console.log(output.stats.minifiedSize); // optimized content size console.log(output.stats.timeSpent); // time spent on optimizations in milliseconds -console.log(output.stats.efficiency); // a ratio of output size to input size (e.g. 25% if content was reduced from 100 bytes to 75 bytes) +console.log(output.stats.efficiency); // `(originalSize - minifiedSize) / originalSize`, e.g. 0.25 if size is reduced from 100 bytes to 75 bytes ``` The `minify` method also accepts an input source map, e.g. diff --git a/lib/optimizer/level-1/optimize.js b/lib/optimizer/level-1/optimize.js index 82cb9531d..ebc3c2415 100644 --- a/lib/optimizer/level-1/optimize.js +++ b/lib/optimizer/level-1/optimize.js @@ -29,6 +29,7 @@ var DEFAULT_ROUNDING_PRECISION = require('../../options/rounding-precision').DEF var WHOLE_PIXEL_VALUE = /(?:^|\s|\()(-?\d+)px/; var TIME_VALUE = /^(\-?[\d\.]+)(m?s)$/; +var HEX_VALUE_PATTERN = /[0-9a-f]/i; var PROPERTY_NAME_PATTERN = /^(?:\-chrome\-|\-[\w\-]+\w|\w[\w\-]+\w|\-\-\S+)$/; var IMPORT_PREFIX_PATTERN = /^@import/i; var QUOTED_PATTERN = /^('.*'|".*")$/; @@ -98,11 +99,15 @@ function optimizeColors(name, value, compatibility) { .replace(/hsl\((-?\d+),(-?\d+)%?,(-?\d+)%?\)/g, function (match, hue, saturation, lightness) { return shortenHsl(hue, saturation, lightness); }) - .replace(/(^|[^='"])#([0-9a-f]{6})($|[^0-9a-f])/gi, function (match, prefix, color, suffix) { - if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) { - return (prefix + '#' + color[0] + color[2] + color[4]).toLowerCase() + suffix; + .replace(/(^|[^='"])#([0-9a-f]{6})/gi, function (match, prefix, color, at, inputValue) { + var suffix = inputValue[at + match.length]; + + if (suffix && HEX_VALUE_PATTERN.test(suffix)) { + return match; + } else if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) { + return (prefix + '#' + color[0] + color[2] + color[4]).toLowerCase(); } else { - return (prefix + '#' + color).toLowerCase() + suffix; + return (prefix + '#' + color).toLowerCase(); } }) .replace(/(^|[^='"])#([0-9a-f]{3})/gi, function (match, prefix, color) { diff --git a/lib/reader/read-sources.js b/lib/reader/read-sources.js index c9173ed62..1338f6adc 100644 --- a/lib/reader/read-sources.js +++ b/lib/reader/read-sources.js @@ -288,7 +288,6 @@ function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) { path.resolve(inlinerContext.rebaseTo, uri); var relativeToCurrentPath = path.relative(currentPath, absoluteUri); var importedStyles; - var importedTokens; var isAllowed = isAllowedResource(uri, false, inlinerContext.inline); var normalizedPath = normalizePath(relativeToCurrentPath); var isLoaded = normalizedPath in inlinerContext.externalContext.sourcesContent; @@ -316,10 +315,14 @@ function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) { inlinerContext.externalContext.sourcesContent[normalizedPath] = importedStyles; inlinerContext.externalContext.stats.originalSize += importedStyles.length; - importedTokens = fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (tokens) { return tokens; }); - importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); + return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) { + importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); - inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); + inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); + inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); + + return doInlineImports(inlinerContext); + }); } inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); diff --git a/lib/tokenizer/tokenize.js b/lib/tokenizer/tokenize.js index 018b89de8..c20e0b39b 100644 --- a/lib/tokenizer/tokenize.js +++ b/lib/tokenizer/tokenize.js @@ -97,6 +97,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { var wasCommentStart = false; var isCommentEnd; var wasCommentEnd = false; + var isCommentEndMarker; var isEscaped; var wasEscaped = false; var seekingValue = false; @@ -111,7 +112,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { isNewLineNix = character == Marker.NEW_LINE_NIX; isNewLineWin = character == Marker.NEW_LINE_NIX && source[position.index - 1] == Marker.NEW_LINE_WIN; isCommentStart = !wasCommentEnd && level != Level.COMMENT && !isQuoted && character == Marker.ASTERISK && source[position.index - 1] == Marker.FORWARD_SLASH; - isCommentEnd = !wasCommentStart && level == Level.COMMENT && character == Marker.FORWARD_SLASH && source[position.index - 1] == Marker.ASTERISK; + isCommentEndMarker = !wasCommentStart && !isQuoted && character == Marker.FORWARD_SLASH && source[position.index - 1] == Marker.ASTERISK; + isCommentEnd = level == Level.COMMENT && isCommentEndMarker; metadata = buffer.length === 0 ? [position.line, position.column, position.source] : @@ -147,6 +149,9 @@ function intoTokens(source, externalContext, internalContext, isNested) { level = levels.pop(); metadata = metadatas.pop() || null; buffer = buffers.pop() || []; + } else if (isCommentEndMarker && source[position.index + 1] != Marker.ASTERISK) { + externalContext.warnings.push('Unexpected \'*/\' at ' + formatPosition([position.line, position.column, position.source]) + '.'); + buffer = []; } else if (character == Marker.SINGLE_QUOTE && !isQuoted) { // single quotation start, e.g. a[href^='https<-- levels.push(level); diff --git a/package.json b/package.json index 83450d0ad..def796cb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clean-css", - "version": "4.1.7", + "version": "4.1.8", "author": "Jakub Pawlowicz (http://twitter.com/jakubpawlowicz)", "description": "A well-tested CSS minifier", "license": "MIT", diff --git a/test/optimizer/level-1/optimize-test.js b/test/optimizer/level-1/optimize-test.js index 9dd549d15..7f2d477e1 100644 --- a/test/optimizer/level-1/optimize-test.js +++ b/test/optimizer/level-1/optimize-test.js @@ -401,6 +401,10 @@ vows.describe('level 1 optimizations') 'a{color:#FFF}', 'a{color:#fff}' ], + 'uppercase long hex to lowercase hex inside gradient 1234': [ + '.block{background-image:linear-gradient(to top,#AABBCC,#FFFFFF)}', + '.block{background-image:linear-gradient(to top,#abc,#fff)}' + ], '4-value hex': [ '.block{color:#0f0a}', '.block{color:#0f0a}' diff --git a/test/protocol-imports-test.js b/test/protocol-imports-test.js index 8144c440a..2d315f45a 100644 --- a/test/protocol-imports-test.js +++ b/test/protocol-imports-test.js @@ -472,6 +472,27 @@ vows.describe('protocol imports').addBatch({ nock.cleanAll(); } }, + 'of a remote resource referenced from local one given via hash': { + topic: function () { + this.reqMocks = nock('http://127.0.0.1') + .get('/remote.css') + .reply(200, 'div{padding:0}'); + + new CleanCSS({ inline: 'all' }).minify({ 'local.css': { styles: '@import url(http://127.0.0.1/remote.css);' } }, this.callback); + }, + 'should not raise errors': function (errors, minified) { + assert.isNull(errors); + }, + 'should process @import': function (errors, minified) { + assert.equal(minified.styles, 'div{padding:0}'); + }, + 'hits the endpoint': function () { + assert.isTrue(this.reqMocks.isDone()); + }, + teardown: function () { + nock.cleanAll(); + } + }, 'of a remote resource after content and no callback': { topic: function () { var source = '.one{color:red}@import url(http://127.0.0.1/remote.css);'; diff --git a/test/tokenizer/tokenize-test.js b/test/tokenizer/tokenize-test.js index fdef4354c..bb752b61b 100644 --- a/test/tokenizer/tokenize-test.js +++ b/test/tokenizer/tokenize-test.js @@ -1015,6 +1015,80 @@ vows.describe(tokenize) ] ] ], + 'two comments one inside another and two rules': [ + '.block-1{color:red;/* comment 1 /* comment 2 */ */}.block-2{color:blue}', + [ + [ + 'rule', + [ + [ + 'rule-scope', + '.block-1', + [ + [1, 0, undefined] + ] + ] + ], + [ + [ + 'property', + [ + 'property-name', + 'color', + [ + [1, 9, undefined] + ] + ], + [ + 'property-value', + 'red', + [ + [1, 15, undefined] + ] + ] + ], + [ + 'comment', + '/* comment 1 /* comment 2 */', + [ + [1, 19, undefined] + ] + ] + ] + ], + [ + 'rule', + [ + [ + 'rule-scope', + '.block-2', + [ + [1, 51, undefined] + ] + ] + ], + [ + [ + 'property', + [ + 'property-name', + 'color', + [ + [1, 60, undefined] + ] + ], + [ + 'property-value', + 'blue', + [ + [1, 66, undefined] + ] + ] + ] + ] + ] + ] + ], 'rule wrapped between comments': [ '/* comment 1 */div/* comment 2 */{color:red}', [