See #895 - ignoring specific styles.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 7 Jun 2017 12:23:27 +0000 (14:23 +0200)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 16 Jun 2017 18:05:12 +0000 (20:05 +0200)
Why:

* Allows parts of CSS document to be wrapped between
  /* clean-css ignore:start */ and /* clean-css ignore:end */ comments
  passing them to output untouched by parsing and optimizing;
* in case of some special stylesheets when optimizations can break
  styling.

History.md
README.md
lib/tokenizer/token.js
lib/tokenizer/tokenize.js
lib/writer/helpers.js
test/integration-test.js
test/tokenizer/tokenize-test.js

index 42b91da..c6eee7d 100644 (file)
@@ -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)
 ==================
index 802f82c..fc2dfa4 100644 (file)
--- 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 `/*`:
index acd0154..a1d726f 100644 (file)
@@ -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`
 };
index 7c071dd..4cde136 100644 (file)
@@ -28,6 +28,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 +62,7 @@ function intoTokens(source, externalContext, internalContext, isNested) {
   var buffer = [];
   var buffers = [];
   var serializedBuffer;
+  var serializedBufferPart;
   var roundBracketLevel = 0;
   var isQuoted;
   var isSpace;
@@ -71,9 +74,11 @@ function intoTokens(source, externalContext, internalContext, isNested) {
   var wasCommentEnd = false;
   var isEscaped;
   var wasEscaped = false;
+  var isRaw = false;
   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 && isRaw) {
+      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);
+
+      isRaw = true;
+      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);
+
+      isRaw = false;
+      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];
 
index ab08633..7ee55b8 100644 (file)
@@ -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));
index 0130e31..4cf2a27 100644 (file)
@@ -429,6 +429,22 @@ 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 }'
+      ],
+      'clean-css ignore comments with nested block': [
+        '/* clean-css ignore:start */ @media print { a { *!color:transparent } } /* clean-css ignore:end */',
+        ' @media print { a { *!color:transparent } } '
       ]
     })
   )
index 583fbad..0002ab9 100644 (file)
@@ -1065,6 +1065,94 @@ vows.describe(tokenize)
           ]
         ]
       ],
+      'rule wrapped between ignore comments': [
+        '.block-1 { color: red }\n/* clean-css ignore:start */\n .block-2 { color: transparent } \n/* clean-css ignore:end */\n.block-3 { color: red }',
+        [
+          [
+            'rule',
+            [
+              [
+                'rule-scope',
+                '.block-1',
+                [
+                  [1, 0, undefined]
+                ]
+              ]
+            ],
+            [
+              [
+                'property',
+                [
+                  'property-name',
+                  'color',
+                  [
+                    [1, 11, undefined]
+                  ]
+                ],
+                [
+                  'property-value',
+                  'red',
+                  [
+                    [1, 18, undefined]
+                  ]
+                ]
+              ]
+            ]
+          ],
+          [
+            'comment',
+            '/* clean-css ignore:start */',
+            [
+              [2, 0, undefined]
+            ]
+          ],
+          [
+            'raw',
+            '\n .block-2 { color: transparent } \n',
+            [
+              [2, 28, undefined]
+            ]
+          ],
+          [
+            'comment',
+            '/* clean-css ignore:end */',
+            [
+              [4, 0, undefined]
+            ]
+          ],
+          [
+            'rule',
+            [
+              [
+                'rule-scope',
+                '.block-3',
+                [
+                  [5, 0, undefined]
+                ]
+              ]
+            ],
+            [
+              [
+                'property',
+                [
+                  'property-name',
+                  'color',
+                  [
+                    [5, 11, undefined]
+                  ]
+                ],
+                [
+                  'property-value',
+                  'red',
+                  [
+                    [5, 18, undefined]
+                  ]
+                ]
+              ]
+            ]
+          ]
+        ]
+      ],
       'two properties wrapped between comments': [
         'div{/* comment 1 */color:red/* comment 2 */}',
         [