Skip to content
This repository
Browse code

Fixes #46 - special characters in URLs and attributes.

* Simplified preserving 'content' attribute's content.
  • Loading branch information...
commit 57b0fcabce916102ca4c079c308b704a0b3b4511 1 parent aa67adf
Goal Smashers Dev Team authored

Showing 3 changed files with 128 additions and 106 deletions. Show diff stats Hide diff stats

  1. +6 0 History.md
  2. +114 104 lib/clean.js
  3. +8 2 test/unit-test.js
6 History.md
Source Rendered
... ... @@ -1,3 +1,9 @@
  1 +1.0 / 2013-xx-xx
  2 +==================
  3 +
  4 +* Fixed issue [#46](https://github.com/GoalSmashers/clean-css/issues/46) - preserving special characters in URLs
  5 + and attributes.
  6 +
1 7 0.10.1 / 2013-02-14
2 8 ==================
3 9
218 lib/clean.js
@@ -31,7 +31,8 @@ var CleanCSS = {
31 31 process: function(data, options) {
32 32 var context = {
33 33 specialComments: [],
34   - contentBlocks: []
  34 + freeTextBlocks: [],
  35 + urlBlocks: []
35 36 };
36 37 var replace = function() {
37 38 if (typeof arguments[0] == 'function')
@@ -74,12 +75,7 @@ var CleanCSS = {
74 75 data = CleanCSS._stripComments(context, data);
75 76 });
76 77
77   - // replace content: with a placeholder
78   - replace(function stripContent() {
79   - data = CleanCSS._stripContent(context, data);
80   - });
81   -
82   - // strip url's parentheses if possible (no spaces inside)
  78 + // strip parentheses in urls if possible (no spaces inside)
83 79 replace(/url\(['"]([^\)]+)['"]\)/g, function(urlMatch) {
84 80 if (urlMatch.match(/\s/g) !== null)
85 81 return urlMatch;
@@ -87,6 +83,47 @@ var CleanCSS = {
87 83 return urlMatch.replace(/\(['"]/, '(').replace(/['"]\)$/, ')');
88 84 });
89 85
  86 + // strip parentheses in animation & font names
  87 + replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) {
  88 + return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1');
  89 + });
  90 +
  91 + // strip parentheses in @keyframes
  92 + replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
  93 + prefix = prefix || '';
  94 + return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
  95 + });
  96 +
  97 + // IE shorter filters, but only if single (IE 7 issue)
  98 + replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
  99 + return filter.toLowerCase() + args + suffix;
  100 + });
  101 +
  102 + // strip parentheses in attribute values
  103 + replace(/\[([^\]]+)\]/g, function(match, content) {
  104 + var eqIndex = content.indexOf('=');
  105 + if (eqIndex < 0 && content.indexOf('\'') < 0 && content.indexOf('"') < 0)
  106 + return match;
  107 +
  108 + var key = content.substring(0, eqIndex);
  109 + var value = content.substring(eqIndex + 1, content.length);
  110 +
  111 + if (/^['"](?:[a-zA-Z][a-zA-Z\d\-]+)['"]$/.test(value))
  112 + return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
  113 + else
  114 + return match;
  115 + });
  116 +
  117 + // replace all free text content with a placeholder
  118 + replace(function stripFreeText() {
  119 + data = CleanCSS._stripFreeText(context, data);
  120 + });
  121 +
  122 + // replace url(...) with a placeholder
  123 + replace(function stripUrls() {
  124 + data = CleanCSS._stripUrls(context, data);
  125 + });
  126 +
90 127 // line breaks
91 128 if (!options.keepBreaks)
92 129 replace(/[\r]?\n/g, ' ');
@@ -119,32 +156,6 @@ var CleanCSS = {
119 156 // trailing semicolons
120 157 replace(/;\}/g, '}');
121 158
122   - // strip quotation in animation & font names
123   - replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) {
124   - return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1');
125   - });
126   -
127   - // strip quotation in @keyframes
128   - replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
129   - prefix = prefix || '';
130   - return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
131   - });
132   -
133   - // strip quotation in attribute values
134   - replace(/\[([^\]]+)\]/g, function(match, content) {
135   - var eqIndex = content.indexOf('=');
136   - if (eqIndex < 0 && content.indexOf('\'') < 0 && content.indexOf('"') < 0)
137   - return match;
138   -
139   - var key = content.substring(0, eqIndex);
140   - var value = content.substring(eqIndex + 1, content.length);
141   -
142   - if (/^['"](?:[a-zA-Z][a-zA-Z\d\-]+)['"]$/.test(value))
143   - return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
144   - else
145   - return match;
146   - });
147   -
148 159 // rgb to hex colors
149 160 replace(/rgb\s*\(([^\)]+)\)/g, function(match, color) {
150 161 var parts = color.split(',');
@@ -184,11 +195,6 @@ var CleanCSS = {
184 195 return match;
185 196 });
186 197
187   - // IE shorter filters, but only if single (IE 7 issue)
188   - replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
189   - return filter.toLowerCase() + args + suffix;
190   - });
191   -
192 198 // zero + unit to zero
193 199 replace(/(\s|:|,)0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0');
194 200 replace(/rect\(0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, 'rect(0');
@@ -284,12 +290,21 @@ var CleanCSS = {
284 290 // remove universal selector when not needed (*#id, *.class etc)
285 291 replace(/\*([\.#:\[])/g, '$1');
286 292
287   - // Restore special comments, content content, and spaces inside calc back
288   - var specialCommentsCount = context.specialComments.length;
289   -
  293 + // Restore spaces inside calc back
290 294 replace(/calc\([^\}]+\}/g, function(match) {
291 295 return match.replace(/\+/g, ' + ');
292 296 });
  297 +
  298 + // Restore urls, content content, and special comments (in that order)
  299 + replace(/__URL__/g, function() {
  300 + return context.urlBlocks.shift();
  301 + });
  302 +
  303 + replace(/__CSSFREETEXT__/g, function() {
  304 + return context.freeTextBlocks.shift();
  305 + });
  306 +
  307 + var specialCommentsCount = context.specialComments.length;
293 308 replace(/__CSSCOMMENT__/g, function() {
294 309 switch (options.keepSpecialComments) {
295 310 case '*':
@@ -302,9 +317,6 @@ var CleanCSS = {
302 317 return '';
303 318 }
304 319 });
305   - replace(/__CSSCONTENT__/g, function() {
306   - return context.contentBlocks.shift();
307   - });
308 320
309 321 // trim spaces at beginning and end
310 322 return data.trim();
@@ -314,10 +326,10 @@ var CleanCSS = {
314 326 // for further restoring. Plain comments are removed. It's done by scanning datq using
315 327 // String#indexOf scanning instead of regexps to speed up the process.
316 328 _stripComments: function(context, data) {
317   - var tempData = [],
318   - nextStart = 0,
319   - nextEnd = 0,
320   - cursor = 0;
  329 + var tempData = [];
  330 + var nextStart = 0;
  331 + var nextEnd = 0;
  332 + var cursor = 0;
321 333
322 334 for (; nextEnd < data.length; ) {
323 335 nextStart = data.indexOf('/*', nextEnd);
@@ -339,75 +351,73 @@ var CleanCSS = {
339 351 data;
340 352 },
341 353
342   - // Strip content tags by replacing them by the __CSSCONTENT__
  354 + // Strip content tags by replacing them by the __CSSFREETEXT__
343 355 // marker for further restoring. It's done via string scanning
344 356 // instead of regexps to speed up the process.
345   - _stripContent: function(context, data) {
346   - var tempData = [],
347   - nextStart = 0,
348   - nextEnd = 0,
349   - cursor = 0,
350   - matchedParenthesis = null;
351   - var allowedPrefixes = [' ', '{', ';', this.lineBreak];
352   - var skipBy = 'content'.length;
353   -
354   - // Find either first (matchedParenthesis == null) or second matching
355   - // parenthesis so that we can determine boundaries of content block.
356   - var nextParenthesis = function(pos) {
357   - var min,
358   - max = data.length;
359   -
360   - if (matchedParenthesis) {
361   - min = data.indexOf(matchedParenthesis, pos);
362   - if (min == -1)
363   - min = max;
364   - } else {
365   - var next1 = data.indexOf("'", pos);
366   - var next2 = data.indexOf('"', pos);
367   - if (next1 == -1)
368   - next1 = max;
369   - if (next2 == -1)
370   - next2 = max;
371   -
372   - min = next1 > next2 ? next2 : next1;
373   - }
  357 + _stripFreeText: function(context, data) {
  358 + var tempData = [];
  359 + var nextStart = 0;
  360 + var nextEnd = 0;
  361 + var cursor = 0;
  362 + var matchedParenthesis = null;
  363 + var singleParenthesis = "'";
  364 + var doubleParenthesis = '"';
  365 + var dataLength = data.length;
374 366
375   - if (min == max)
376   - return -1;
  367 + for (; nextEnd < data.length; ) {
  368 + var nextStartSingle = data.indexOf(singleParenthesis, nextEnd + 1);
  369 + var nextStartDouble = data.indexOf(doubleParenthesis, nextEnd + 1);
377 370
378   - if (matchedParenthesis) {
379   - matchedParenthesis = null;
380   - return min;
381   - } else {
382   - // check if there's anything else between pos and min
383   - // that doesn't match ':' or whitespace
384   - if (/[^:\s]/.test(data.substring(pos, min)))
385   - return -1;
  371 + if (nextStartSingle == -1)
  372 + nextStartSingle = dataLength;
  373 + if (nextStartDouble == -1)
  374 + nextStartDouble = dataLength;
386 375
387   - matchedParenthesis = data.charAt(min);
388   - return min + 1;
  376 + if (nextStartSingle < nextStartDouble) {
  377 + nextStart = nextStartSingle;
  378 + matchedParenthesis = singleParenthesis;
  379 + } else {
  380 + nextStart = nextStartDouble;
  381 + matchedParenthesis = doubleParenthesis;
389 382 }
390   - };
391 383
392   - for (; nextEnd < data.length; ) {
393   - nextStart = data.indexOf('content', nextEnd);
394 384 if (nextStart == -1)
395 385 break;
396 386
397   - // skip by `skipBy` bytes if matched declaration is not a property but ID, class name or a some substring
398   - if (allowedPrefixes.indexOf(data[nextStart - 1]) == -1) {
399   - nextEnd += skipBy;
400   - continue;
401   - }
402   -
403   - nextStart = nextParenthesis(nextStart + skipBy);
404   - nextEnd = nextParenthesis(nextStart);
  387 + nextEnd = data.indexOf(matchedParenthesis, nextStart + 1);
405 388 if (nextStart == -1 || nextEnd == -1)
406 389 break;
407 390
408   - tempData.push(data.substring(cursor, nextStart - 1));
409   - tempData.push('__CSSCONTENT__');
410   - context.contentBlocks.push(data.substring(nextStart - 1, nextEnd + 1));
  391 + tempData.push(data.substring(cursor, nextStart));
  392 + tempData.push('__CSSFREETEXT__');
  393 + context.freeTextBlocks.push(data.substring(nextStart, nextEnd + 1));
  394 + cursor = nextEnd + 1;
  395 + }
  396 +
  397 + return tempData.length > 0 ?
  398 + tempData.join('') + data.substring(cursor, data.length) :
  399 + data;
  400 + },
  401 +
  402 + // Strip urls by replacing them by the __URL__
  403 + // marker for further restoring. It's done via string scanning
  404 + // instead of regexps to speed up the process.
  405 + _stripUrls: function(context, data) {
  406 + var nextStart = 0;
  407 + var nextEnd = 0;
  408 + var cursor = 0;
  409 + var tempData = [];
  410 +
  411 + for (; nextEnd < data.length; ) {
  412 + nextStart = data.indexOf('url(', nextEnd);
  413 + if (nextStart == -1)
  414 + break;
  415 +
  416 + nextEnd = data.indexOf(')', nextStart);
  417 +
  418 + tempData.push(data.substring(cursor, nextStart));
  419 + tempData.push('__URL__');
  420 + context.urlBlocks.push(data.substring(nextStart, nextEnd + 1));
411 421 cursor = nextEnd + 1;
412 422 }
413 423
10 test/unit-test.js
@@ -528,7 +528,10 @@ vows.describe('clean-units').addBatch({
528 528 'not add a space before url\'s hash': [
529 529 "url(\"../fonts/d90b3358-e1e2-4abb-ba96-356983a54c22.svg#d90b3358-e1e2-4abb-ba96-356983a54c22\")",
530 530 "url(../fonts/d90b3358-e1e2-4abb-ba96-356983a54c22.svg#d90b3358-e1e2-4abb-ba96-356983a54c22)"
531   - ]
  531 + ],
  532 + 'keep urls from being stripped down #1': 'a{background:url(/image-1.0.png)}',
  533 + 'keep urls from being stripped down #2': "a{background:url(/image-white.png)}",
  534 + 'keep __URL__ in comments (so order is important)': '/*! __URL__ */a{}'
532 535 }),
533 536 'fonts': cssContext({
534 537 'keep format quotation': "@font-face{font-family:PublicVintage;src:url(./PublicVintage.otf) format('opentype')}",
@@ -603,7 +606,10 @@ vows.describe('clean-units').addBatch({
603 606 'should strip quotations if is less specific selectors': [
604 607 'a[data-href*=\'object1\']{border-color:red}a[data-href|=\'object2\']{border-color:#0f0}',
605 608 'a[data-href*=object1]{border-color:red}a[data-href|=object2]{border-color:#0f0}'
606   - ]
  609 + ],
  610 + 'should keep special characters inside attributes #1': "a[data-css='color:white']",
  611 + 'should keep special characters inside attributes #2': "a[data-text='a\nb\nc']",
  612 + 'should keep special characters inside attributes #3': 'a[href="/version-0.01.html"]'
607 613 }),
608 614 'ie filters': cssContext({
609 615 'short alpha': [

0 comments on commit 57b0fca

Please sign in to comment.
Something went wrong with that request. Please try again.