diff --git a/clean-css.js b/clean-css.js new file mode 100644 index 0000000..719864d --- /dev/null +++ b/clean-css.js @@ -0,0 +1,551 @@ +/** + * Clean-css - https://github.com/GoalSmashers/clean-css + * Released under the terms of MIT license + * + * Copyright (C) 2011-2013 GoalSmashers.com + */ + +var fs = require('fs'); +var path = require('path'); +var existsSync = fs.existsSync || path.existsSync; + +var CleanCSS = { + colors: { + toHex: { + aqua: '#0ff', + black: '#000', + blue: '#00f', + fuchsia: '#f0f', + white: '#fff', + yellow: '#ff0' + }, + toName: { + '#000080': 'navy', + '#008000': 'green', + '#008080': 'teal', + '#800000': 'maroon', + '#800080': 'purple', + '#808000': 'olive', + '#808080': 'gray', + '#c0c0c0': 'silver', + '#f00': 'red' + } + }, + + process: function(data, options) { + var context = { + specialComments: [], + freeTextBlocks: [], + urlBlocks: [] + }; + var replace = function() { + if (typeof arguments[0] == 'function') + arguments[0](); + else + data = data.replace.apply(data, arguments); + }; + var lineBreak = process.platform == 'win32' ? '\r\n' : '\n'; + this.lineBreak = lineBreak; + + options = options || {}; + + // * - leave all important comments + // 1 - leave first important comment only + // 0 - strip all important comments + options.keepSpecialComments = 'keepSpecialComments' in options ? + options.keepSpecialComments : + '*'; + + options.keepBreaks = options.keepBreaks || false; + + // replace function + if (options.debug) { + var originalReplace = replace; + replace = function(pattern, replacement) { + var name = typeof pattern == 'function' ? + /function (\w+)\(/.exec(pattern.toString())[1] : + pattern; + + var start = process.hrtime(); + originalReplace(pattern, replacement); + + var itTook = process.hrtime(start); + console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000.0); + }; + } + + // replace all escaped line breaks + replace(/\\(\r\n|\n)/mg, ''); + + // inline all imports + replace(function inlineImports() { + data = CleanCSS._inlineImports(data, { + root: options.root || process.cwd(), + relativeTo: options.relativeTo + }); + }); + + // strip comments one by one + replace(function stripComments() { + data = CleanCSS._stripComments(context, data); + }); + + // strip parentheses in urls if possible (no spaces inside) + replace(/url\((['"])([^\)]+)['"]\)/g, function(match, quote, url) { + if (url.match(/[ \t]/g) !== null) + return 'url(' + quote + url + quote + ')'; + else + return 'url(' + url + ')'; + }); + + // strip parentheses in animation & font names + replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) { + return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1'); + }); + + // strip parentheses in @keyframes + replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) { + prefix = prefix || ''; + return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, '')); + }); + + // IE shorter filters, but only if single (IE 7 issue) + replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) { + return filter.toLowerCase() + args + suffix; + }); + + // strip parentheses in attribute values + replace(/\[([^\]]+)\]/g, function(match, content) { + var eqIndex = content.indexOf('='); + if (eqIndex < 0 && content.indexOf('\'') < 0 && content.indexOf('"') < 0) + return match; + + var key = content.substring(0, eqIndex); + var value = content.substring(eqIndex + 1, content.length); + + if (/^['"](?:[a-zA-Z][a-zA-Z\d\-_]+)['"]$/.test(value)) + return '[' + key + '=' + value.substring(1, value.length - 1) + ']'; + else + return match; + }); + + // replace all free text content with a placeholder + replace(function stripFreeText() { + data = CleanCSS._stripFreeText(context, data); + }); + + // replace url(...) with a placeholder + replace(function stripUrls() { + data = CleanCSS._stripUrls(context, data); + }); + + // line breaks + if (!options.keepBreaks) + replace(/[\r]?\n/g, ' '); + + // multiple whitespace + replace(/[\t ]+/g, ' '); + + // multiple semicolons (with optional whitespace) + replace(/;[ ]?;+/g, ';'); + + // multiple line breaks to one + replace(/ (?:\r\n|\n)/g, lineBreak); + replace(/(?:\r\n|\n)+/g, lineBreak); + + // remove spaces around selectors + replace(/ ([+~>]) /g, '$1'); + + // remove extra spaces inside content + replace(/([!\(\{\}:;=,\n]) /g, '$1'); + replace(/ ([!\)\{\};=,\n])/g, '$1'); + replace(/(?:\r\n|\n)\}/g, '}'); + replace(/([\{;,])(?:\r\n|\n)/g, '$1'); + replace(/ :([^\{\};]+)([;}])/g, ':$1$2'); + + // restore spaces inside IE filters (IE 7 issue) + replace(/progid:[^(]+\(([^\)]+)/g, function(match) { + return match.replace(/,/g, ', '); + }); + + // trailing semicolons + replace(/;\}/g, '}'); + + // hsl to hex colors + replace(/hsl\((\d+),(\d+)%?,(\d+)%?\)/g, function(match, hue, saturation, lightness) { + var asRgb = CleanCSS._hslToRgb(hue, saturation, lightness); + var redAsHex = asRgb[0].toString(16); + var greenAsHex = asRgb[1].toString(16); + var blueAsHex = asRgb[2].toString(16); + + return '#' + + ((redAsHex.length == 1 ? '0' : '') + redAsHex) + + ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) + + ((blueAsHex.length == 1 ? '0' : '') + blueAsHex); + }); + + // rgb to hex colors + replace(/rgb\((\d+),(\d+),(\d+)\)/g, function(match, red, green, blue) { + var redAsHex = parseInt(red, 10).toString(16); + var greenAsHex = parseInt(green, 10).toString(16); + var blueAsHex = parseInt(blue, 10).toString(16); + + return '#' + + ((redAsHex.length == 1 ? '0' : '') + redAsHex) + + ((greenAsHex.length == 1 ? '0' : '') + greenAsHex) + + ((blueAsHex.length == 1 ? '0' : '') + blueAsHex); + }); + + // long hex to short hex colors + replace(/([,: \(])#([0-9a-f]{6})/gi, function(match, prefix, color) { + if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) + return prefix + '#' + color[0] + color[2] + color[4]; + else + return prefix + '#' + color; + }); + + // replace color name with hex values if shorter (or the other way around) + ['toHex', 'toName'].forEach(function(type) { + var pattern = '(' + Object.keys(CleanCSS.colors[type]).join('|') + ')'; + var colorSwitcher = function(match, prefix, colorValue, suffix) { + return prefix + CleanCSS.colors[type][colorValue.toLowerCase()] + suffix; + }; + replace(new RegExp('([ :,\\(])' + pattern + '([;\\}!\\) ])', 'ig'), colorSwitcher); + replace(new RegExp('(,)' + pattern + '(,)', 'ig'), colorSwitcher); + }); + + // replace font weight with numerical value + replace(/(font|font\-weight):(normal|bold)([ ;\}!])/g, function(match, property, weight, suffix) { + if (weight == 'normal') + return property + ':400' + suffix; + else if (weight == 'bold') + return property + ':700' + suffix; + else + return match; + }); + + // zero + unit to zero + replace(/(\s|:|,)0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0'); + replace(/rect\(0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, 'rect(0'); + + // fraction zeros removal + replace(/\.([1-9]*)0+(\D)/g, function(match, nonZeroPart, suffix) { + return (nonZeroPart ? '.' : '') + nonZeroPart + suffix; + }); + + // restore 0% in hsl/hsla + replace(/(hsl|hsla)\(([^\)]+)\)/g, function(match, colorFunction, colorDef) { + var tokens = colorDef.split(','); + if (tokens[1] == '0') + tokens[1] = '0%'; + if (tokens[2] == '0') + tokens[2] = '0%'; + return colorFunction + '(' + tokens.join(',') + ')'; + }); + + // none to 0 + replace(/(border|border-top|border-right|border-bottom|border-left|outline):none/g, '$1:0'); + + // background:none to 0 + replace(/(background):none([;}])/g, '$1:0$2'); + + // multiple zeros into one + replace(/:0 0 0 0([^\.])/g, ':0$1'); + replace(/([: ,=\-])0\.(\d)/g, '$1.$2'); + + // shorthand notations + var shorthandRegex = function(repeats, hasSuffix) { + var pattern = '(padding|margin|border\\-width|border\\-color|border\\-style|border\\-radius):'; + for (var i = 0; i < repeats; i++) { + pattern += '([\\d\\w\\.%#\\(\\),]+)' + (i < repeats - 1 ? ' ' : ''); + } + return new RegExp(pattern + (hasSuffix ? '([;}])' : ''), 'g'); + }; + + // 4 size values into less + replace(shorthandRegex(4), function(match, property, size1, size2, size3, size4) { + if (size1 === size2 && size1 === size3 && size1 === size4) + return property + ':' + size1; + else if (size1 === size3 && size2 === size4) + return property + ':' + size1 + ' ' + size2; + else if (size2 === size4) + return property + ':' + size1 + ' ' + size2 + ' ' + size3; + else + return match; + }); + + // 3 size values into less + replace(shorthandRegex(3, true), function(match, property, size1, size2, size3, suffix) { + if (size1 === size2 && size1 === size3) + return property + ':' + size1 + suffix; + else if (size1 === size3) + return property + ':' + size1 + ' ' + size2 + suffix; + else + return match; + }); + + // same 2 values into one + replace(shorthandRegex(2, true), function(match, property, size1, size2, suffix) { + if (size1 === size2) + return property + ':' + size1 + suffix; + else + return match; + }); + + // restore rect(...) zeros syntax for 4 zeros + replace(/rect\(\s?0(\s|,)0[ ,]0[ ,]0\s?\)/g, 'rect(0$10$10$10)'); + + if (options.removeEmpty) { + // empty elements + replace(/[^\{\}]+\{\}/g, ''); + + // empty @media declarations + replace(/@media [^\{]+\{\}/g, ''); + } + + // remove universal selector when not needed (*#id, *.class etc) + replace(/\*([\.#:\[])/g, '$1'); + + // Restore spaces inside calc back + replace(/calc\([^\}]+\}/g, function(match) { + return match.replace(/\+/g, ' + '); + }); + + // Restore urls, content content, and special comments (in that order) + replace(/__URL__/g, function() { + return context.urlBlocks.shift(); + }); + + replace(/__CSSFREETEXT__/g, function() { + return context.freeTextBlocks.shift(); + }); + + var specialCommentsCount = context.specialComments.length; + var breakSuffix = options.keepBreaks ? lineBreak : ''; + replace(new RegExp('__CSSCOMMENT__(' + lineBreak + '| )?', 'g'), function() { + switch (options.keepSpecialComments) { + case '*': + return context.specialComments.shift() + breakSuffix; + case 1: + return context.specialComments.length == specialCommentsCount ? + context.specialComments.shift() + breakSuffix : + ''; + case 0: + return ''; + } + }); + + // move first charset to the beginning + replace(function moveCharset() { + // get first charset in stylesheet + var match = data.match(/@charset [^;]+;/); + var firstCharset = match ? match[0] : null; + if (!firstCharset) + return; + + // reattach first charset and remove all subsequent + data = firstCharset + + (options.keepBreaks ? lineBreak : '') + + data.replace(new RegExp('@charset [^;]+;(' + lineBreak + ')?', 'g'), ''); + }); + + // trim spaces at beginning and end + return data.trim(); + }, + + // Inlines all imports taking care of repetitions, unknown files, and cilcular dependencies + _inlineImports: function(data, options) { + var tempData = []; + var nextStart = 0; + var nextEnd = 0; + var cursor = 0; + + options.relativeTo = options.relativeTo || options.root; + options.visited = options.visited || []; + + var inlinedFile = function() { + var importedFile = data + .substring(data.indexOf('(', nextStart) + 1, nextEnd) + .replace(/['"]/g, ''); + + if (/^(http|https):\/\//.test(importedFile)) + return "@import url(" + importedFile + ");"; + + var relativeTo = importedFile[0] == '/' ? + options.root : + options.relativeTo; + + var fullPath = path.resolve(path.join(relativeTo, importedFile)); + + if (existsSync(fullPath) && fs.statSync(fullPath).isFile() && options.visited.indexOf(fullPath) == -1) { + options.visited.push(fullPath); + + var importedData = fs.readFileSync(fullPath, 'utf8'); + return CleanCSS._inlineImports(importedData, { + root: options.root, + relativeTo: path.dirname(fullPath), + visited: options.visited + }); + } else { + return ''; + } + }; + + for (; nextEnd < data.length; ) { + nextStart = data.indexOf('@import url(', cursor); + if (nextStart == -1) + break; + + nextEnd = data.indexOf(')', nextStart); + if (nextEnd == -1) + break; + + tempData.push(data.substring(cursor, nextStart)); + tempData.push(inlinedFile()); + cursor = nextEnd + 2; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; + }, + + // Strip special comments (/*! ... */) by replacing them by __CSSCOMMENT__ marker + // for further restoring. Plain comments are removed. It's done by scanning datq using + // String#indexOf scanning instead of regexps to speed up the process. + _stripComments: function(context, data) { + var tempData = []; + var nextStart = 0; + var nextEnd = 0; + var cursor = 0; + + for (; nextEnd < data.length; ) { + nextStart = data.indexOf('/*', nextEnd); + nextEnd = data.indexOf('*/', nextStart + 2); + if (nextStart == -1 || nextEnd == -1) + break; + + tempData.push(data.substring(cursor, nextStart)); + if (data[nextStart + 2] == '!') { + // in case of special comments, replace them with a placeholder + context.specialComments.push(data.substring(nextStart, nextEnd + 2)); + tempData.push('__CSSCOMMENT__'); + } + cursor = nextEnd + 2; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; + }, + + // Strip content tags by replacing them by the __CSSFREETEXT__ + // marker for further restoring. It's done via string scanning + // instead of regexps to speed up the process. + _stripFreeText: function(context, data) { + var tempData = []; + var nextStart = 0; + var nextEnd = 0; + var cursor = 0; + var matchedParenthesis = null; + var singleParenthesis = "'"; + var doubleParenthesis = '"'; + var dataLength = data.length; + + for (; nextEnd < data.length; ) { + var nextStartSingle = data.indexOf(singleParenthesis, nextEnd + 1); + var nextStartDouble = data.indexOf(doubleParenthesis, nextEnd + 1); + + if (nextStartSingle == -1) + nextStartSingle = dataLength; + if (nextStartDouble == -1) + nextStartDouble = dataLength; + + if (nextStartSingle < nextStartDouble) { + nextStart = nextStartSingle; + matchedParenthesis = singleParenthesis; + } else { + nextStart = nextStartDouble; + matchedParenthesis = doubleParenthesis; + } + + if (nextStart == -1) + break; + + nextEnd = data.indexOf(matchedParenthesis, nextStart + 1); + if (nextStart == -1 || nextEnd == -1) + break; + + tempData.push(data.substring(cursor, nextStart)); + tempData.push('__CSSFREETEXT__'); + context.freeTextBlocks.push(data.substring(nextStart, nextEnd + 1)); + cursor = nextEnd + 1; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; + }, + + // Strip urls by replacing them by the __URL__ + // marker for further restoring. It's done via string scanning + // instead of regexps to speed up the process. + _stripUrls: function(context, data) { + var nextStart = 0; + var nextEnd = 0; + var cursor = 0; + var tempData = []; + + for (; nextEnd < data.length; ) { + nextStart = data.indexOf('url(', nextEnd); + if (nextStart == -1) + break; + + nextEnd = data.indexOf(')', nextStart); + + tempData.push(data.substring(cursor, nextStart)); + tempData.push('__URL__'); + context.urlBlocks.push(data.substring(nextStart, nextEnd + 1)); + cursor = nextEnd + 1; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; + }, + + // HSL to RGB converter. Both methods taken and adapted from: + // http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript + _hslToRgb: function(h, s, l) { + var r, g, b; + + h = ~~h / 360; + s = ~~s / 100; + l = ~~l / 100; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + var q = l < 0.5 ? + l * (1 + s) : + l + s - l * s; + var p = 2 * l - q; + r = this._hueToRgb(p, q, h + 1/3); + g = this._hueToRgb(p, q, h); + b = this._hueToRgb(p, q, h - 1/3); + } + + return [~~(r * 255), ~~(g * 255), ~~(b * 255)]; + }, + + _hueToRgb: function(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } +}; + +module.exports = CleanCSS; diff --git a/package.json b/package.json index a0b59a1..37acf5f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "author" : "Jairaj Sethi (http://jairaj.org/)", "name" : "zerver", "description" : "client-integrated webapp server", - "version" : "0.10.0", + "version" : "0.10.1", "license" : "MIT" , "repository" : { "type" : "git", @@ -15,7 +15,6 @@ "dependencies" : { "mime" : "1.2.7" , "uglify-js" : "1.3.4" , - "clean-css" : "1.0.0" , "less" : "1.2.2" }, "devDependencies" : { diff --git a/zerver.js b/zerver.js index 595109f..ca1622e 100755 --- a/zerver.js +++ b/zerver.js @@ -1,7 +1,7 @@ /* Imports and static vars */ var less = require('less'), - clean = require('clean-css'), + clean = require(__dirname + '/clean-css'), fs = require('fs' ), http = require('http'), mime = require('mime'),