optimize.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. var sortSelectors = require('./sort-selectors');
  2. var tidyRules = require('./tidy-rules');
  3. var tidyBlock = require('./tidy-block');
  4. var tidyAtRule = require('./tidy-at-rule');
  5. var Hack = require('../hack');
  6. var removeUnused = require('../remove-unused');
  7. var restoreFromOptimizing = require('../restore-from-optimizing');
  8. var wrapForOptimizing = require('../wrap-for-optimizing').all;
  9. var configuration = require('../configuration');
  10. var optimizers = require('./value-optimizers');
  11. var OptimizationLevel = require('../../options/optimization-level').OptimizationLevel;
  12. var Token = require('../../tokenizer/token');
  13. var Marker = require('../../tokenizer/marker');
  14. var formatPosition = require('../../utils/format-position');
  15. var serializeRules = require('../../writer/one-time').rules;
  16. var CHARSET_TOKEN = '@charset';
  17. var CHARSET_REGEXP = new RegExp('^' + CHARSET_TOKEN, 'i');
  18. var DEFAULT_ROUNDING_PRECISION = require('../../options/rounding-precision').DEFAULT;
  19. var VARIABLE_PROPERTY_NAME_PATTERN = /^--\S+$/;
  20. var PROPERTY_NAME_PATTERN = /^(?:-chrome-|-[\w-]+\w|\w[\w-]+\w|\w{1,})$/;
  21. var IMPORT_PREFIX_PATTERN = /^@import/i;
  22. var URL_PREFIX_PATTERN = /^url\(/i;
  23. function startsAsUrl(value) {
  24. return URL_PREFIX_PATTERN.test(value);
  25. }
  26. function isImport(token) {
  27. return IMPORT_PREFIX_PATTERN.test(token[1]);
  28. }
  29. function isLegacyFilter(property) {
  30. var value;
  31. if (property.name == 'filter' || property.name == '-ms-filter') {
  32. value = property.value[0][1];
  33. return value.indexOf('progid') > -1
  34. || value.indexOf('alpha') === 0
  35. || value.indexOf('chroma') === 0;
  36. }
  37. return false;
  38. }
  39. function noop() {}
  40. function noopValueOptimizer(_name, value, _options) { return value; }
  41. function optimizeBody(rule, properties, context) {
  42. var options = context.options;
  43. var valueOptimizers;
  44. var property, name, type, value;
  45. var propertyToken;
  46. var propertyOptimizer;
  47. var serializedRule = serializeRules(rule);
  48. var _properties = wrapForOptimizing(properties);
  49. var pluginValueOptimizers = context.options.plugins.level1Value;
  50. var pluginPropertyOptimizers = context.options.plugins.level1Property;
  51. var isVariable;
  52. var i, l;
  53. for (i = 0, l = _properties.length; i < l; i++) {
  54. var j, k, m, n;
  55. property = _properties[i];
  56. name = property.name;
  57. propertyOptimizer = configuration[name] && configuration[name].propertyOptimizer || noop;
  58. valueOptimizers = configuration[name] && configuration[name].valueOptimizers || [optimizers.whiteSpace];
  59. isVariable = VARIABLE_PROPERTY_NAME_PATTERN.test(name);
  60. if (isVariable) {
  61. valueOptimizers = options.variableOptimizers.length > 0
  62. ? options.variableOptimizers
  63. : [optimizers.whiteSpace];
  64. }
  65. if (!isVariable && !PROPERTY_NAME_PATTERN.test(name)) {
  66. propertyToken = property.all[property.position];
  67. context.warnings.push('Invalid property name \'' + name + '\' at ' + formatPosition(propertyToken[1][2][0]) + '. Ignoring.');
  68. property.unused = true;
  69. continue;
  70. }
  71. if (property.value.length === 0) {
  72. propertyToken = property.all[property.position];
  73. context.warnings.push('Empty property \'' + name + '\' at ' + formatPosition(propertyToken[1][2][0]) + '. Ignoring.');
  74. property.unused = true;
  75. continue;
  76. }
  77. if (property.hack && (
  78. (property.hack[0] == Hack.ASTERISK || property.hack[0] == Hack.UNDERSCORE)
  79. && !options.compatibility.properties.iePrefixHack
  80. || property.hack[0] == Hack.BACKSLASH && !options.compatibility.properties.ieSuffixHack
  81. || property.hack[0] == Hack.BANG && !options.compatibility.properties.ieBangHack)) {
  82. property.unused = true;
  83. continue;
  84. }
  85. if (!options.compatibility.properties.ieFilters && isLegacyFilter(property)) {
  86. property.unused = true;
  87. continue;
  88. }
  89. if (property.block) {
  90. optimizeBody(rule, property.value[0][1], context);
  91. continue;
  92. }
  93. for (j = 0, m = property.value.length; j < m; j++) {
  94. type = property.value[j][0];
  95. value = property.value[j][1];
  96. if (type == Token.PROPERTY_BLOCK) {
  97. property.unused = true;
  98. context.warnings.push('Invalid value token at ' + formatPosition(value[0][1][2][0]) + '. Ignoring.');
  99. break;
  100. }
  101. if (startsAsUrl(value) && !context.validator.isUrl(value)) {
  102. property.unused = true;
  103. context.warnings.push('Broken URL \'' + value + '\' at ' + formatPosition(property.value[j][2][0]) + '. Ignoring.');
  104. break;
  105. }
  106. for (k = 0, n = valueOptimizers.length; k < n; k++) {
  107. value = valueOptimizers[k](name, value, options);
  108. }
  109. for (k = 0, n = pluginValueOptimizers.length; k < n; k++) {
  110. value = pluginValueOptimizers[k](name, value, options);
  111. }
  112. property.value[j][1] = value;
  113. }
  114. propertyOptimizer(serializedRule, property, options);
  115. for (j = 0, m = pluginPropertyOptimizers.length; j < m; j++) {
  116. pluginPropertyOptimizers[j](serializedRule, property, options);
  117. }
  118. }
  119. restoreFromOptimizing(_properties);
  120. removeUnused(_properties);
  121. removeComments(properties, options);
  122. }
  123. function removeComments(tokens, options) {
  124. var token;
  125. var i;
  126. for (i = 0; i < tokens.length; i++) {
  127. token = tokens[i];
  128. if (token[0] != Token.COMMENT) {
  129. continue;
  130. }
  131. optimizeComment(token, options);
  132. if (token[1].length === 0) {
  133. tokens.splice(i, 1);
  134. i--;
  135. }
  136. }
  137. }
  138. function optimizeComment(token, options) {
  139. if (token[1][2] == Marker.EXCLAMATION && (options.level[OptimizationLevel.One].specialComments == 'all' || options.commentsKept < options.level[OptimizationLevel.One].specialComments)) {
  140. options.commentsKept++;
  141. return;
  142. }
  143. token[1] = [];
  144. }
  145. function cleanupCharsets(tokens) {
  146. var hasCharset = false;
  147. for (var i = 0, l = tokens.length; i < l; i++) {
  148. var token = tokens[i];
  149. if (token[0] != Token.AT_RULE) { continue; }
  150. if (!CHARSET_REGEXP.test(token[1])) { continue; }
  151. if (hasCharset || token[1].indexOf(CHARSET_TOKEN) == -1) {
  152. tokens.splice(i, 1);
  153. i--;
  154. l--;
  155. } else {
  156. hasCharset = true;
  157. tokens.splice(i, 1);
  158. tokens.unshift([Token.AT_RULE, token[1].replace(CHARSET_REGEXP, CHARSET_TOKEN)]);
  159. }
  160. }
  161. }
  162. function buildUnitRegexp(options) {
  163. var units = ['px', 'em', 'ex', 'cm', 'mm', 'in', 'pt', 'pc', '%'];
  164. var otherUnits = ['ch', 'rem', 'vh', 'vm', 'vmax', 'vmin', 'vw'];
  165. otherUnits.forEach(function(unit) {
  166. if (options.compatibility.units[unit]) {
  167. units.push(unit);
  168. }
  169. });
  170. return new RegExp('(^|\\s|\\(|,)0(?:' + units.join('|') + ')(\\W|$)', 'g');
  171. }
  172. function buildPrecisionOptions(roundingPrecision) {
  173. var precisionOptions = {
  174. matcher: null,
  175. units: {}
  176. };
  177. var optimizable = [];
  178. var unit;
  179. var value;
  180. for (unit in roundingPrecision) {
  181. value = roundingPrecision[unit];
  182. if (value != DEFAULT_ROUNDING_PRECISION) {
  183. precisionOptions.units[unit] = {};
  184. precisionOptions.units[unit].value = value;
  185. precisionOptions.units[unit].multiplier = 10 ** value;
  186. optimizable.push(unit);
  187. }
  188. }
  189. if (optimizable.length > 0) {
  190. precisionOptions.enabled = true;
  191. precisionOptions.decimalPointMatcher = new RegExp('(\\d)\\.($|' + optimizable.join('|') + ')($|\\W)', 'g');
  192. precisionOptions.zeroMatcher = new RegExp('(\\d*)(\\.\\d+)(' + optimizable.join('|') + ')', 'g');
  193. }
  194. return precisionOptions;
  195. }
  196. function buildVariableOptimizers(options) {
  197. return options.level[OptimizationLevel.One].variableValueOptimizers.map(function(optimizer) {
  198. if (typeof (optimizer) == 'string') {
  199. return optimizers[optimizer] || noopValueOptimizer;
  200. }
  201. return optimizer;
  202. });
  203. }
  204. function level1Optimize(tokens, context) {
  205. var options = context.options;
  206. var levelOptions = options.level[OptimizationLevel.One];
  207. var ie7Hack = options.compatibility.selectors.ie7Hack;
  208. var adjacentSpace = options.compatibility.selectors.adjacentSpace;
  209. var spaceAfterClosingBrace = options.compatibility.properties.spaceAfterClosingBrace;
  210. var format = options.format;
  211. var mayHaveCharset = false;
  212. var afterRules = false;
  213. options.unitsRegexp = options.unitsRegexp || buildUnitRegexp(options);
  214. options.precision = options.precision || buildPrecisionOptions(levelOptions.roundingPrecision);
  215. options.commentsKept = options.commentsKept || 0;
  216. options.variableOptimizers = options.variableOptimizers || buildVariableOptimizers(options);
  217. for (var i = 0, l = tokens.length; i < l; i++) {
  218. var token = tokens[i];
  219. switch (token[0]) {
  220. case Token.AT_RULE:
  221. token[1] = isImport(token) && afterRules ? '' : token[1];
  222. token[1] = levelOptions.tidyAtRules ? tidyAtRule(token[1]) : token[1];
  223. mayHaveCharset = true;
  224. break;
  225. case Token.AT_RULE_BLOCK:
  226. optimizeBody(token[1], token[2], context);
  227. afterRules = true;
  228. break;
  229. case Token.NESTED_BLOCK:
  230. token[1] = levelOptions.tidyBlockScopes ? tidyBlock(token[1], spaceAfterClosingBrace) : token[1];
  231. level1Optimize(token[2], context);
  232. afterRules = true;
  233. break;
  234. case Token.COMMENT:
  235. optimizeComment(token, options);
  236. break;
  237. case Token.RULE:
  238. token[1] = levelOptions.tidySelectors
  239. ? tidyRules(token[1], !ie7Hack, adjacentSpace, format, context.warnings)
  240. : token[1];
  241. token[1] = token[1].length > 1 ? sortSelectors(token[1], levelOptions.selectorsSortingMethod) : token[1];
  242. optimizeBody(token[1], token[2], context);
  243. afterRules = true;
  244. break;
  245. }
  246. if (token[0] == Token.COMMENT
  247. && token[1].length === 0
  248. || levelOptions.removeEmpty
  249. && (token[1].length === 0 || (token[2] && token[2].length === 0))) {
  250. tokens.splice(i, 1);
  251. i--;
  252. l--;
  253. }
  254. }
  255. if (levelOptions.cleanupCharsets && mayHaveCharset) {
  256. cleanupCharsets(tokens);
  257. }
  258. return tokens;
  259. }
  260. module.exports = level1Optimize;