diff --git a/History.md b/History.md index 42b91da9b..c6eee7d61 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,7 @@ * Adds `process` method for compatibility with optimize-css-assets-webpack-plugin. * Fixed issue [#861](https://github.com/jakubpawlowicz/clean-css/issues/861) - new `transition` property optimizer. +* Fixed issue [#895](https://github.com/jakubpawlowicz/clean-css/issues/895) - ignoring specific styles. [4.1.4 / 2017-06-14](https://github.com/jakubpawlowicz/clean-css/compare/v4.1.3...v4.1.4) ================== diff --git a/README.md b/README.md index 802f82c58..fc2dfa450 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ According to [tests](http://goalsmashers.github.io/css-minification-benchmark/) * [How to process remote `@import`s correctly?](#how-to-process-remote-imports-correctly) * [How to apply arbitrary transformations to CSS properties?](#how-to-apply-arbitrary-transformations-to-css-properties) * [How to specify a custom rounding precision?](#how-to-specify-a-custom-rounding-precision) + * [How to keep a CSS fragment intact?](#how-to-keep-a-css-fragment-intact) * [How to preserve a comment block?](#how-to-preserve-a-comment-block) * [How to rebase relative image URLs?](#how-to-rebase-relative-image-urls) * [How to work with source maps?](#how-to-work-with-source-maps) @@ -119,6 +120,7 @@ clean-css 4.2 will introduce the following changes / features: * Adds `process` method for compatibility with optimize-css-assets-webpack-plugin; * new `transition` property optimizer; +* preserves any CSS content between `/* clean-css ignore:start */` and `/* clean-css ignore:end */` comments; ## Constructor options @@ -561,6 +563,34 @@ new CleanCSS({ which sets all units rounding precision to 3 digits except `px` unit precision of 5 digits. +## How to keep a CSS fragment intact? + +Wrap the CSS fragment in special comments which instruct clean-css to preserve it, e.g. + +```css +.block-1 { + color: red +} +/* clean-css ignore:start */ +.block-special { + color: transparent +} +/* clean-css ignore:end */ +.block-2 { + margin: 0 +} +``` + +Optimizing this CSS will result in the following output: + +```css +.block-1{color:red} +.block-special { + color: transparent +} +.block-2{margin:0} +``` + ## How to preserve a comment block? Use the `/*!` notation instead of the standard one `/*`: diff --git a/lib/tokenizer/token.js b/lib/tokenizer/token.js index acd0154ee..a1d726f0e 100644 --- a/lib/tokenizer/token.js +++ b/lib/tokenizer/token.js @@ -9,6 +9,7 @@ var Token = { PROPERTY_BLOCK: 'property-block', // e.g. `--var:{color:red}` PROPERTY_NAME: 'property-name', // e.g. `color` PROPERTY_VALUE: 'property-value', // e.g. `red` + RAW: 'raw', // e.g. anything between /* clean-css ignore:start */ and /* clean-css ignore:end */ comments RULE: 'rule', // e.g `div > a{...}` RULE_SCOPE: 'rule-scope' // e.g `div > a` }; diff --git a/lib/tokenizer/tokenize.js b/lib/tokenizer/tokenize.js index 7c071dd93..be268cf11 100644 --- a/lib/tokenizer/tokenize.js +++ b/lib/tokenizer/tokenize.js @@ -7,6 +7,7 @@ var Level = { BLOCK: 'block', COMMENT: 'comment', DOUBLE_QUOTE: 'double-quote', + RAW: 'raw', RULE: 'rule', SINGLE_QUOTE: 'single-quote' }; @@ -28,6 +29,8 @@ var BLOCK_RULES = [ '@supports' ]; +var IGNORE_END_COMMENT_PATTERN = /\/\* clean\-css ignore:end \*\/$/; +var IGNORE_START_COMMENT_PATTERN = /^\/\* clean\-css ignore:start \*\//; var REPEAT_PATTERN = /^\[\s*\d+\s*\]$/; var RULE_WORD_SEPARATOR_PATTERN = /[\s\(]/; var TAIL_BROKEN_VALUE_PATTERN = /[\s|\}]*$/; @@ -60,6 +63,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { var buffer = []; var buffers = []; var serializedBuffer; + var serializedBufferPart; var roundBracketLevel = 0; var isQuoted; var isSpace; @@ -74,6 +78,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { var seekingValue = false; var seekingPropertyBlockClosing = false; var position = internalContext.position; + var lastCommentStartAt; for (; position.index < source.length; position.index++) { var character = source[position.index]; @@ -94,6 +99,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { buffer.push(character); } else if (!isCommentEnd && level == Level.COMMENT) { buffer.push(character); + } else if (!isCommentStart && !isCommentEnd && level == Level.RAW) { + buffer.push(character); } else if (isCommentStart && (level == Level.BLOCK || level == Level.RULE) && buffer.length > 1) { // comment start within block preceded by some content, e.g. div/*<-- metadatas.push(metadata); @@ -110,6 +117,33 @@ function intoTokens(source, externalContext, internalContext, isNested) { levels.push(level); level = Level.COMMENT; buffer.push(character); + } else if (isCommentEnd && isIgnoreStartComment(buffer)) { + // ignore:start comment end, e.g. /* clean-css ignore:start */<-- + serializedBuffer = buffer.join('').trim() + character; + lastToken = [Token.COMMENT, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]; + newTokens.push(lastToken); + levels.push(Level.RAW); + + level = levels.pop(); + metadata = metadatas.pop() || null; + buffer = buffers.pop() || []; + } else if (isCommentEnd && isIgnoreEndComment(buffer)) { + // ignore:start comment end, e.g. /* clean-css ignore:end */<-- + serializedBuffer = buffer.join('') + character; + lastCommentStartAt = serializedBuffer.lastIndexOf(Marker.FORWARD_SLASH + Marker.ASTERISK); + + serializedBufferPart = serializedBuffer.substring(0, lastCommentStartAt); + lastToken = [Token.RAW, serializedBufferPart, [originalMetadata(metadata, serializedBufferPart, externalContext)]]; + newTokens.push(lastToken); + + serializedBufferPart = serializedBuffer.substring(lastCommentStartAt); + metadata = [position.line, position.column - serializedBufferPart.length + 1, position.source]; + lastToken = [Token.COMMENT, serializedBufferPart, [originalMetadata(metadata, serializedBufferPart, externalContext)]]; + newTokens.push(lastToken); + + level = levels.pop(); + metadata = metadatas.pop() || null; + buffer = buffers.pop() || []; } else if (isCommentEnd) { // comment end, e.g. /* comment */<-- serializedBuffer = buffer.join('').trim() + character; @@ -434,6 +468,14 @@ function intoTokens(source, externalContext, internalContext, isNested) { return allTokens; } +function isIgnoreStartComment(buffer) { + return IGNORE_START_COMMENT_PATTERN.test(buffer.join('') + Marker.FORWARD_SLASH); +} + +function isIgnoreEndComment(buffer) { + return IGNORE_END_COMMENT_PATTERN.test(buffer.join('') + Marker.FORWARD_SLASH); +} + function originalMetadata(metadata, value, externalContext, selectorFallbacks) { var source = metadata[2]; diff --git a/lib/writer/helpers.js b/lib/writer/helpers.js index ab08633e8..7ee55b893 100644 --- a/lib/writer/helpers.js +++ b/lib/writer/helpers.js @@ -95,6 +95,9 @@ function property(context, tokens, position, lastPropertyAt) { store(context, colon(context)); value(context, token); store(context, needsSemicolon ? semicolon(context, Breaks.AfterProperty, isLast) : emptyCharacter); + break; + case Token.RAW: + store(context, token); } } @@ -200,6 +203,9 @@ function all(context, tokens) { store(context, token); store(context, allowsBreak(context, Breaks.AfterComment) ? lineBreak : emptyCharacter); break; + case Token.RAW: + store(context, token); + break; case Token.RULE: rules(context, token[1]); store(context, openBrace(context, Breaks.AfterRuleBegins, true)); diff --git a/test/integration-test.js b/test/integration-test.js index 0130e3165..26bef2c31 100644 --- a/test/integration-test.js +++ b/test/integration-test.js @@ -429,6 +429,18 @@ vows.describe('integration tests') 'two comments, general selector right after first, and quotes': [ '/*! comment */*{box-sizing:border-box}div:before{content:" "}/*! @comment */div{display:inline-block}', '/*! comment */*{box-sizing:border-box}div:before{content:" "}/*! @comment */div{display:inline-block}' + ], + 'clean-css ignore comments on top level': [ + '/* clean-css ignore:start */\n .block { color:transparent } \n/* clean-css ignore:end */', + '\n .block { color:transparent } \n' + ], + 'clean-css ignore comments on nested block level': [ + '@media print { /* clean-css ignore:start */\n .block { color:transparent } \n/* clean-css ignore:end */ }', + '@media print{\n .block { color:transparent } \n}' + ], + 'clean-css ignore comments on rule level': [ + '.block { /* clean-css ignore:start */ *!color:transparent /* clean-css ignore:end */ }', + '.block{ *!color:transparent }' ] }) ) diff --git a/test/tokenizer/tokenize-test.js b/test/tokenizer/tokenize-test.js index 583fbadf5..a2068a891 100644 --- a/test/tokenizer/tokenize-test.js +++ b/test/tokenizer/tokenize-test.js @@ -1065,6 +1065,32 @@ vows.describe(tokenize) ] ] ], + 'rule wrapped between ignore comments': [ + '/* clean-css ignore:start */\n .block { color:transparent } \n/* clean-css ignore:end */', + [ + [ + 'comment', + '/* clean-css ignore:start */', + [ + [1, 0, undefined] + ] + ], + [ + 'raw', + '\n .block { color:transparent } \n', + [ + [1, 28, undefined] + ] + ], + [ + 'comment', + '/* clean-css ignore:end */', + [ + [3, 0, undefined] + ] + ] + ] + ], 'two properties wrapped between comments': [ 'div{/* comment 1 */color:red/* comment 2 */}', [