Skip to content

Commit

Permalink
See #895 - ignoring specific styles.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jakubpawlowicz committed Jun 16, 2017
1 parent 595cd47 commit 95a788d
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 0 deletions.
1 change: 1 addition & 0 deletions History.md
Expand Up @@ -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)
==================
Expand Down
30 changes: 30 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 `/*`:
Expand Down
1 change: 1 addition & 0 deletions lib/tokenizer/token.js
Expand Up @@ -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`
};
Expand Down
42 changes: 42 additions & 0 deletions lib/tokenizer/tokenize.js
Expand Up @@ -7,6 +7,7 @@ var Level = {
BLOCK: 'block',
COMMENT: 'comment',
DOUBLE_QUOTE: 'double-quote',
RAW: 'raw',
RULE: 'rule',
SINGLE_QUOTE: 'single-quote'
};
Expand All @@ -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|\}]*$/;
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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];

Expand Down
6 changes: 6 additions & 0 deletions lib/writer/helpers.js
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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));
Expand Down
12 changes: 12 additions & 0 deletions test/integration-test.js
Expand Up @@ -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 }'
]
})
)
Expand Down
26 changes: 26 additions & 0 deletions test/tokenizer/tokenize-test.js
Expand Up @@ -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 */}',
[
Expand Down

0 comments on commit 95a788d

Please sign in to comment.