├── CNAME ├── test ├── css │ ├── mod │ │ ├── mod4.css │ │ ├── mod2.css │ │ ├── mod1.css │ │ ├── mod3.css │ │ ├── test_rel_src_mod.css │ │ ├── deep_mod │ │ │ └── deep_import.css │ │ └── mod5_data_uri.css │ ├── external │ │ └── ext_mod.css │ ├── image │ │ ├── yest.png │ │ ├── yest1.png │ │ ├── one-piece.jpg │ │ └── one-piece1.jpg │ ├── test.combo.css │ ├── test.source.css │ ├── test2.combo.css │ ├── font │ │ ├── uxiconfont.eot │ │ └── uxiconfont.ttf │ ├── test4.source.css │ ├── test3.source.css │ ├── a2u.source.css │ ├── a2u.combo.css │ ├── test2.source.css │ └── test3.combo.css └── css-combo-test.js ├── .travis.yml ├── index.html ├── .gitignore ├── examples ├── simple-build.js └── multiple-build.js ├── LICENSE.md ├── package.json ├── README.md ├── lib ├── index.js ├── utils.js ├── combo.js └── cssmin.js └── bin └── csscombo /CNAME: -------------------------------------------------------------------------------- 1 | csscombo.com 2 | -------------------------------------------------------------------------------- /test/css/mod/mod4.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/css/external/ext_mod.css: -------------------------------------------------------------------------------- 1 | .ext-mods { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /test/css/mod/mod2.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | h1 { 3 | height: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /test/css/mod/mod1.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/mod/mod1.css -------------------------------------------------------------------------------- /test/css/mod/mod3.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/mod/mod3.css -------------------------------------------------------------------------------- /test/css/image/yest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/image/yest.png -------------------------------------------------------------------------------- /test/css/test.combo.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/test.combo.css -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.11 5 | - 7 6 | - 6 7 | - 5 8 | - 4 -------------------------------------------------------------------------------- /test/css/image/yest1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/image/yest1.png -------------------------------------------------------------------------------- /test/css/test.source.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/test.source.css -------------------------------------------------------------------------------- /test/css/test2.combo.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/test2.combo.css -------------------------------------------------------------------------------- /test/css/font/uxiconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/font/uxiconfont.eot -------------------------------------------------------------------------------- /test/css/font/uxiconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/font/uxiconfont.ttf -------------------------------------------------------------------------------- /test/css/image/one-piece.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/image/one-piece.jpg -------------------------------------------------------------------------------- /test/css/image/one-piece1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/image/one-piece1.jpg -------------------------------------------------------------------------------- /test/css/mod/test_rel_src_mod.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxingplay/css-combo/HEAD/test/css/mod/test_rel_src_mod.css -------------------------------------------------------------------------------- /test/css/test4.source.css: -------------------------------------------------------------------------------- 1 | @import "//g.tbcdn.cn/??tb/global/3.3.35/global-min.css"; 2 | @import "//g.tbcdn.cn/??tb/item-detail/4.1.0/index-min.css"; -------------------------------------------------------------------------------- /test/css/test3.source.css: -------------------------------------------------------------------------------- 1 | /** 2 | * test3.source.css 3 | * @author: daxingplay 4 | * @date: 13-10-26 5 | */ 6 | 7 | @import "mod/mod5_data_uri.css"; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Combo - A new way to combine css modules 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/css/a2u.source.css: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | /* 中文注释 */ 3 | body{ 4 | font-family:'Helvetica Neue',Helvetica,Arial,Sans-serif; 5 | font-size:24px; 6 | } 7 | div{ 8 | font-family:"华文楷体"; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | *.iml 11 | .settings/ 12 | pids 13 | logs 14 | results 15 | .idea 16 | 17 | node_modules 18 | npm-debug.log 19 | *.combo.css -------------------------------------------------------------------------------- /test/css/a2u.combo.css: -------------------------------------------------------------------------------- 1 | /* 2 | combined files : 3 | 4 | */ 5 | 6 | 7 | /* \4E2D\6587\6CE8\91CA */ 8 | body{ 9 | font-family:'Helvetica Neue',Helvetica,Arial,Sans-serif; 10 | font-size:24px; 11 | } 12 | div{ 13 | font-family:"STKaiti"; 14 | } 15 | -------------------------------------------------------------------------------- /test/css/test2.source.css: -------------------------------------------------------------------------------- 1 | /*comment test*/ 2 | /*@import "./mod/mod1.css";*/ 3 | @import "./mod/mod1.css"; 4 | @import "./mod/mod2.css"; 5 | @import "./mod/mod3.css"; 6 | @import "./mod/mod4.css"; 7 | /*! this comment needs to be preserved. */ 8 | @import url("http://assets.taobaocdn.com/tbsp/tbsp.source.css"); 9 | 10 | @import "./mod/test_rel_src_mod.css"; 11 | 12 | body { 13 | background: transparent; 14 | } -------------------------------------------------------------------------------- /examples/simple-build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author: daxingplay 4 | * @time: 13-4-17 15:26 5 | * @description: 6 | */ 7 | 8 | var path = require('path'), 9 | combo = require('../lib/index'); 10 | 11 | combo.build( 12 | path.resolve(__dirname, '../test/css/test4.source.css'), 13 | path.resolve(__dirname, '../test/css/'), 14 | { 15 | debug: true 16 | }, 17 | function(err, result){ 18 | console.log(result); 19 | } 20 | ); -------------------------------------------------------------------------------- /test/css/mod/deep_mod/deep_import.css: -------------------------------------------------------------------------------- 1 | /** 2 | * style For Page 3 | */ 4 | 5 | /*this is deep include */ 6 | @font-face { 7 | font-family: BorderWeb; 8 | src:url(../../font/uxiconfont.eot); 9 | } 10 | @font-face { 11 | font-family: Kingston; 12 | src:url(../../font/uxiconfont.ttf); 13 | } 14 | 15 | html { 16 | background:url(../../image/yest.png) repeat-x; 17 | } 18 | 19 | a { 20 | background: url(../../image/one-piece.jpg); 21 | } 22 | 23 | li { 24 | list-style: url(../../image/yest.png) none; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/multiple-build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author: daxingplay 4 | * @time: 13-4-17 15:26 5 | * @description: 6 | */ 7 | 8 | var path = require('path'), 9 | combo = require('../lib/index'); 10 | 11 | combo.build( 12 | [ 13 | path.resolve(__dirname, '../test/css/test.source.css'), 14 | path.resolve(__dirname, '../test/css/test2.source.css') 15 | ], 16 | path.resolve(__dirname, '../test/css/'), 17 | { 18 | debug: true 19 | }, 20 | function(err, result){ 21 | console.log(result); 22 | } 23 | ); -------------------------------------------------------------------------------- /test/css/mod/mod5_data_uri.css: -------------------------------------------------------------------------------- 1 | /** 2 | * mod5_data_uri 3 | * @author: daxingplay 4 | * @date: 13-10-26 5 | */ 6 | 7 | li { 8 | background: 9 | url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7) 10 | no-repeat 11 | left center; 12 | padding: 5px 0 5px 25px; 13 | } -------------------------------------------------------------------------------- /test/css/test3.combo.css: -------------------------------------------------------------------------------- 1 | /* 2 | combined files : 3 | 4 | mod/mod5_data_uri.css 5 | */ 6 | 7 | /** 8 | * test3.source.css 9 | * @author: daxingplay 10 | * @date: 13-10-26 11 | */ 12 | 13 | 14 | /** 15 | * mod5_data_uri 16 | * @author: daxingplay 17 | * @date: 13-10-26 18 | */ 19 | 20 | li { 21 | background: 22 | url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7) 23 | no-repeat 24 | left center; 25 | padding: 5px 0 5px 25px; 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-combo", 3 | "version": "0.6.0", 4 | "description": "css module combo tool", 5 | "author": "daxingplay ", 6 | "homepage": "", 7 | "keywords": [ 8 | "css", 9 | "build", 10 | "combo" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/daxingplay/css-combo/issues" 14 | }, 15 | "contributors": [], 16 | "engines": { 17 | "node": ">=0.8.1" 18 | }, 19 | "directories": { 20 | "lib": "./lib/" 21 | }, 22 | "main": "./lib/index.js", 23 | "bin": { 24 | "csscombo": "./bin/csscombo" 25 | }, 26 | "dependencies": { 27 | "async": "^0.9.0", 28 | "iconv-lite": ">=0.1.0", 29 | "lodash": "^3.1.0", 30 | "request": "^2.53.0" 31 | }, 32 | "devDependencies": { 33 | "mocha": "~1.4.1", 34 | "should": "~0.6.3" 35 | }, 36 | "scripts": { 37 | "test": "mocha -t 20000" 38 | }, 39 | "licenses": [ 40 | { 41 | "type": "MIT", 42 | "url": "https://github.com/daxingplay/css-combo/blob/master/LICENSE.md" 43 | } 44 | ], 45 | "repository": { 46 | "type": "git", 47 | "url": "git@github.com:daxingplay/css-combo.git" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-combo 2 | 3 | 4 | [![Build Status](https://secure.travis-ci.org/daxingplay/css-combo.png)](http://travis-ci.org/daxingplay/css-combo) 5 | 6 | [![NPM version](https://badge.fury.io/js/css-combo.png)](http://badge.fury.io/js/css-combo) 7 | 8 | ## Introduction 9 | combo css which import other css 10 | 对于js,目前已经有比较成熟的模块化方案,比如seajs、kissy,但是css方面呢,一般是通过less进行编译打包的。less官方对于less文件中的@import "xxx.css"是不会打包进来的,这也是考虑到本身就是有需求要这样引用css,而如果你@import "xxx.less",less打包工具就会分析这些引入的模块,进行打包。 11 | css-combo就是借鉴了这种思想,实现了css模块化。即在入口文件中@import其他模块,然后对入口文件进行打包的时候,该工具会分析import的文件,把这些文件打包进来。 12 | 13 | 对于CSS模块化,欢迎大家看我这篇博文:(http://www.techcheng.com/study/css/introduce-css-combo.html) 14 | 15 | ## Usage 16 | 17 | 首先需要npm安装一下: 18 | 19 | npm install -g css-combo 20 | 21 | ### 命令行使用 ### 22 | 23 | 命令行下,可以先进入需要打包的文件所在目录,然后 24 | 25 | csscombo xxx.source.css xxx.combo.css 26 | 27 | 第一个参数是源文件名,第二个参数是打包之后的文件名 28 | 29 | 其他选项有: 30 | 31 | * -s: silent, 静默模式,表示不输出任何信息 32 | * -ic: inputCharset, 可以指定输入文件的编码 33 | * -oc: outputCharset, 可以指定输出文件的编码 34 | 35 | ### 在NodeJS里面使用 ### 36 | 37 | 你也可以在自己的打包工具中调用css combo,和其他npm包一样: 38 | 39 | var CSSCombo = require('css-combo'); 40 | CSSCombo.build(src, dest, cfg, function(err){ callback(); }); 41 | 42 | * src 入口文件的地址 43 | * dest 输出目录或者输出的完整路径(含文件名,推荐),可以使用相对路径 44 | * cfg 参数可以配置以下选项: 45 | 46 | * inputEncoding:{String} 输入文件编码,可选,默认检测入口文件中的@charset设置。如果入口文件没有设置@charset,那么最好设置本选项 47 | * outputEncoding:{String} 输出文件编码,可选,默认UTF-8 48 | * exclude:{Array} 黑名单正则数组,可选,默认空 49 | * compress: {Boolean} 是否压缩,默认为true,处理规则同YUICompressor 50 | * debug: {Boolean} 是否打印日志 51 | * paths: {Array} `@import`额外查找的路径。 52 | * native2ascii: {Boolean} 是否替换中文为Unicode字符,默认为true 53 | * replaceFont: {Boolean} 是否把中文的字体名称替换成英文名称,默认为true,此选项依赖上面的选项,即必须native2ascii配置为true才有效。强烈建议各位开发者在书写CSS的时候就使用英文名称,比如微软雅黑,写成'Microsoft YaHei' 54 | 55 | ### 在grunt中使用 ### 56 | 57 | CSS Combo配套的grunt插件:https://github.com/daxingplay/grunt-css-combo 58 | 59 | ## TODO 60 | 61 | * 增加目录打包形式 62 | 63 | ## ChangeList 64 | 65 | * 0.6.0:支持webp类型的图片格式 66 | * 0.3.7:修复无法解析CDN Combo类型的URL的问题 67 | * 0.3.6:修复单个src配置多个font路径的问题,修复font里面含有?的问题 68 | * 0.3.5:修复issue #26 69 | * 0.3.4:修复issue #22 70 | * 0.3.3:增加替换中文的字体名称为英文名称的方法 71 | * 0.2.2:修正打包之后输出文件编码问题 72 | * 0.2.7:build参数更改,提供更多形式的输入,去掉部分log信息 73 | 74 | ## License 75 | css-combo 遵守 "MIT":https://github.com/daxingplay/css-combo/blob/master/LICENSE.md 协议 76 | 77 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a css module combo tool 3 | * author: daxingplay(daxingplay@gmail.com) 4 | */ 5 | var fs = require('fs'), 6 | path = require('path'), 7 | _ = require('lodash'), 8 | async = require('async'), 9 | utils = require('./utils'); 10 | 11 | var CssCombo = require('./combo'); 12 | 13 | function _iterator(src, dest, options){ 14 | var files = [], 15 | dests = []; 16 | options = _.isPlainObject(options) ? _.clone(options) : {}; 17 | if(src && _.isPlainObject(src)){ 18 | options = src; 19 | files.push(options.src || options.target); 20 | dests.push(options.dest || options.output); 21 | }else if(_.isString(src)){ 22 | files.push(src); 23 | dests.push(dest); 24 | }else{ 25 | files = src; 26 | dests = dest; 27 | } 28 | //return [files, dests, options]; 29 | return function(iterator, callback){ 30 | var funcs = []; 31 | 32 | _.forEach(files, function(file, index){ 33 | funcs.push(function(cb){ 34 | iterator({ 35 | src: file, 36 | dest: _.isArray(dests) ? (dests[index] || '') : dests, 37 | options: options 38 | }, cb); 39 | }); 40 | }); 41 | async.parallel(funcs, function(err, results){ 42 | callback && callback(err, results); 43 | }); 44 | }; 45 | } 46 | 47 | module.exports = { 48 | build: function(src, dest, options, callback){ 49 | for(var i = 1; i < arguments.length; i++){ 50 | if(_.isFunction(arguments[i])){ 51 | callback = arguments[i]; 52 | break; 53 | } 54 | } 55 | var walker = _iterator(src, dest, options); 56 | walker(function(item, cb){ 57 | var c = _.merge(item.options, { 58 | src: item.src, 59 | dest: item.dest 60 | }); 61 | new CssCombo(c).build(cb); 62 | }, callback); 63 | }, 64 | analyze: function(src, options, callback){ 65 | for(var i = 1; i < arguments.length; i++){ 66 | if(_.isFunction(arguments[i])){ 67 | callback = arguments[i]; 68 | break; 69 | } 70 | } 71 | var walker = _iterator(src, '', options); 72 | walker(function(item, cb){ 73 | var c = _.merge(item.options, { 74 | src: item.src, 75 | dest: item.dest 76 | }); 77 | new CssCombo(c).analyze(cb); 78 | }, callback); 79 | }, 80 | version: require('../package.json').version 81 | }; 82 | -------------------------------------------------------------------------------- /bin/csscombo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'), 4 | fs = require('fs'), 5 | sys = require('util'), 6 | os = require('os'); 7 | 8 | var cssCombo = require('../lib/index'); 9 | var args = process.argv.slice(1); 10 | var options = { 11 | compress: false, 12 | silent: true, 13 | paths: [], 14 | verbose: false, 15 | inputCharset: 'gbk', 16 | outputCharset: 'gbk' 17 | }; 18 | 19 | args = args.filter(function (arg) { 20 | var match, 21 | argValue; 22 | 23 | if (match = arg.match(/^-I(.+)$/)) { 24 | options.paths.push(match[1]); 25 | return false; 26 | } 27 | 28 | if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i)) { 29 | arg = match[1]; 30 | argValue = match[2]; 31 | } else { 32 | return arg; 33 | } 34 | 35 | switch (arg) { 36 | case 'v': 37 | case 'version': 38 | sys.puts("csscombo v" + cssCombo.version + " (CSS Module Compiler) [NodeJS]"); 39 | process.exit(0); 40 | case 'verbose': 41 | options.verbose = true; 42 | break; 43 | case 's': 44 | case 'silent': 45 | options.silent = true; 46 | break; 47 | case 'h': 48 | case 'help': 49 | sys.puts("usage: csscombo source [destination]"); 50 | process.exit(0); 51 | case 'x': 52 | case 'compress': 53 | options.compress = true; 54 | break; 55 | case 'inputCharset': 56 | case 'ic': 57 | options.inputCharset = argValue; 58 | break; 59 | case 'outputCharset': 60 | case 'oc': 61 | options.outputCharset = argValue; 62 | break; 63 | } 64 | }); 65 | 66 | var input = args[1]; 67 | if (input && input != '-') { 68 | input = path.resolve(process.cwd(), input); 69 | } 70 | var output = args[2]; 71 | if (output) { 72 | output = path.resolve(process.cwd(), output); 73 | } 74 | 75 | if (! input) { 76 | sys.puts("csscombo: no input files"); 77 | process.exit(1); 78 | } 79 | 80 | if (input != '-') { 81 | cssCombo.build({ 82 | target: input, 83 | output: output, 84 | debug: options.verbose, 85 | inputCharset: options.inputCharset, 86 | outputCharset: options.outputCharset, 87 | silent: options.verbose ? false : options.silent, 88 | compress: options.compress 89 | }, function(err){ 90 | if(err){ 91 | sys.puts('csscombo: ' + err); 92 | process.exit(1); 93 | }else{ 94 | sys.puts('csscombo: success ' + input + ' ===> ' + output); 95 | process.exit(0); 96 | } 97 | }); 98 | } else { 99 | sys.puts('csscombo: cannot find input file.'); 100 | process.exit(1); 101 | } 102 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var url = require('url'); 3 | var path = require('path'); 4 | var http = require('http'); 5 | var request = require('request'); 6 | 7 | var ZNMAP = { 8 | 9 | '\\534E\\6587\\7EC6\\9ED1': ['STHeiti Light [STXihei]', 'STXiheii'], 10 | '\\534E\\6587\\9ED1\\4F53': 'STHeiti', 11 | '\\4E3D\\9ED1 Pro': 'LiHei Pro Medium', 12 | '\\4E3D\\5B8B Pro': 'LiSong Pro Light', 13 | '\\6807\\6977\\4F53': ["DFKai-SB","BiauKai"], 14 | '\\82F9\\679C\\4E3D\\4E2D\\9ED1': 'Apple LiGothic Medium', 15 | '\\82F9\\679C\\4E3D\\7EC6\\5B8B': 'Apple LiSung Light', 16 | 17 | '\\65B0\\7EC6\\660E\4F53': 'PMingLiU', 18 | '\\7EC6\\660E\\4F53': 'MingLiU', 19 | '\\9ED1\\4F53': 'SimHei', 20 | '\\5B8B\\4F53': 'SimSun', 21 | '\\65B0\\5B8B\\4F53': 'NSimSun', 22 | '\\4EFF\\5B8B': 'FangSong', 23 | '\\6977\\4F53': 'KaiTi', 24 | '\\4EFF\\5B8B_GB2312': 'FangSong_GB2312', 25 | '\\6977\\4F53_GB2312': 'KaiTi_GB2312', 26 | '\\5FAE\\x8F6F\\6B63\\9ED1\\4F53': 'Microsoft JhengHei', 27 | '\\5FAE\\8F6F\\96C5\\9ED1': 'Microsoft YaHei', 28 | 29 | '\\96B6\\4E66': 'LiSu', 30 | '\\5E7C\\5706': 'YouYuan', 31 | '\\534E\\6587\\6977\\4F53': 'STKaiti', 32 | '\\534E\\6587\\5B8B\\4F53': 'STSong', 33 | '\\534E\\6587\\4E2D\\5B8B': 'STZhongsong', 34 | '\\534E\\6587\\4EFF\\5B8B': 'STFangsong', 35 | '\\65B9\\6B63\\8212\\4F53': 'FZShuTi', 36 | '\\65B9\\6B63\\59DA\\4F53': 'FZYaoti', 37 | '\\534E\\6587\\5F69\\4E91': 'STCaiyun', 38 | '\\534E\\6587\\7425\\73C0': 'STHupo', 39 | '\\534E\\6587\\96B6\\4E66': 'STLiti', 40 | '\\534E\\6587\\884C\\6977': 'STXingkai', 41 | '\\534E\\6587\\65B0\\9B4F': 'STXinwei' 42 | }; 43 | 44 | module.exports = { 45 | debug: false, 46 | log: function(msg, type) { 47 | var self = this; 48 | type = type ? type: 'info'; 49 | if (msg && (self.debug || (!self.debug && type != 'debug'))) { 50 | console.log((type ? '[' + type.toUpperCase() + '] ': '') + msg); 51 | } 52 | }, 53 | /** 54 | * analyze @charset first. 55 | * @example: 56 | * 1. @charset 'gbk'; 57 | * 2. @charset "gbk"; 58 | * @link: https://developer.mozilla.org/en/CSS/@charset 59 | */ 60 | detectCharset: function(input) { 61 | var result = /@charset\s+['|"](\w*)["|'];/.exec(input), 62 | charset = 'UTF-8'; 63 | if (result && result[1]) { 64 | charset = result[1]; 65 | } 66 | // else{ 67 | // var detect = jschardet.detect(input); 68 | // if(detect && detect.confidence > 0.9){ 69 | // charset = detect.encoding; 70 | // } 71 | // } 72 | return charset; 73 | }, 74 | mkdirSync: function(dirpath, mode) { 75 | var self = this; 76 | if (!fs.existsSync(dirpath)) { 77 | // try to create parent dir first. 78 | self.mkdirSync(path.dirname(dirpath), mode); 79 | fs.mkdirSync(dirpath, mode); 80 | } 81 | }, 82 | getRemoteFile: function(filePath, callback) { 83 | var self = this; 84 | if(filePath.substr(0, 2) === '//'){ 85 | // TODO this is just a patch. It need to consider more complex situations such as https only. 86 | filePath = 'http:' + filePath; 87 | } 88 | request({ 89 | url: filePath, 90 | headers: { 91 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36' 92 | } 93 | }, function (error, response, body) { 94 | if (!error && response.statusCode == 200) { 95 | var charset = 'utf-8'; 96 | if (typeof response.headers['content-type'] !== 'undefined') { 97 | var regResult = response.headers['content-type'].match(/;charset=(\S+)/); 98 | if (regResult !== null && regResult[1]) { 99 | charset = regResult[1]; 100 | self.log('The charset of url ' + filePath + ' is: ' + charset, 'debug'); 101 | } 102 | } 103 | callback && callback(body, charset); 104 | }else{ 105 | self.log('request for ' + filePath + ' error: ' + error); 106 | } 107 | }); 108 | }, 109 | //中文转unicode 110 | a2u: function(text) { 111 | text = escape(text.toString()).replace(/\+/g, "%2B"); 112 | var matches = text.match(/(%([0-9A-F]{2}))/gi); 113 | if (matches) { 114 | for (var matchid = 0; matchid < matches.length; matchid++) { 115 | var code = matches[matchid].substring(1, 3); 116 | if (parseInt(code, 16) >= 128) { 117 | text = text.replace(matches[matchid], '%u00' + code); 118 | } 119 | } 120 | } 121 | text = text.replace('%25', '%u0025'); 122 | 123 | return text; 124 | }, 125 | unicode2En: function(code) { 126 | if(ZNMAP.hasOwnProperty(code)){ 127 | return ZNMAP[code]; 128 | } 129 | return code; 130 | } 131 | }; 132 | 133 | -------------------------------------------------------------------------------- /test/css-combo-test.js: -------------------------------------------------------------------------------- 1 | var CssCombo = require('../lib/index'), 2 | ComboCore = require('../lib/combo'), 3 | path = require('path'), 4 | fs = require('fs'), 5 | should = require('should'); 6 | 7 | describe('When extract imports ', function(){ 8 | 9 | var extract = function(content){ 10 | var result = ComboCore.prototype.extractImports(content); 11 | return result ? result.filePath : null; 12 | }; 13 | 14 | it('should support tengine combo url format.', function(){ 15 | extract('@import url(http://g.tbcdn.cn/??mui/global/1.2.42/global.css);').should.equal('http://g.tbcdn.cn/??mui/global/1.2.42/global.css'); 16 | extract('@import url("http://g.tbcdn.cn/??mui/global/1.2.42/global.css");').should.equal('http://g.tbcdn.cn/??mui/global/1.2.42/global.css'); 17 | }); 18 | 19 | it('should support remote url.', function(){ 20 | extract('@import url(http://g.tbcdn.cn/mui/global/1.2.42/global.css);').should.equal('http://g.tbcdn.cn/mui/global/1.2.42/global.css'); 21 | }); 22 | 23 | it('should support remote url with params.', function(){ 24 | extract('@import url(http://g.tbcdn.cn/mui/global/1.2.42/global.css?test=123);').should.equal('http://g.tbcdn.cn/mui/global/1.2.42/global.css'); 25 | }); 26 | 27 | it('should support local path.', function(){ 28 | extract('@import url(test/123.css);').should.equal('test/123.css'); 29 | }); 30 | 31 | it('should support path with "".', function(){ 32 | extract('@import url("test/123.css");').should.equal('test/123.css'); 33 | extract('@import url("http://test.com/123.css");').should.equal('http://test.com/123.css'); 34 | }); 35 | 36 | it('should support path without url function.', function(){ 37 | extract('@import "test/123.css";').should.equal('test/123.css'); 38 | extract('@import "http://test.com/123.css";').should.equal('http://test.com/123.css'); 39 | }); 40 | 41 | it('should support simple http url.', function(){ 42 | extract('@import "//test.com/123.css";').should.equal('//test.com/123.css'); 43 | }); 44 | 45 | it('should support custom protocols.', function(){ 46 | extract('@import url("chrome://communicator/skin.css");').should.equal('chrome://communicator/skin.css'); 47 | }); 48 | 49 | it('should support https url.', function(){ 50 | extract('@import "https://test.com/123.css";').should.equal('https://test.com/123.css'); 51 | }); 52 | 53 | it('should support list-of-media-queries.', function(){ 54 | extract('@import url("fineprint.css") print;').should.equal('fineprint.css'); 55 | extract('@import url("bluish.css") projection, tv;').should.equal('bluish.css'); 56 | extract('@import url("landscape.css") screen and (orientation:landscape);').should.equal('landscape.css'); 57 | }); 58 | 59 | }); 60 | 61 | describe('When analyze', function(){ 62 | 63 | it('should get results', function(done){ 64 | CssCombo.analyze({ 65 | target: path.resolve(__dirname, 'css/test.source.css'), 66 | debug: false, 67 | paths: [ path.resolve(__dirname, 'css/external' ) ], 68 | inputEncoding: 'gbk', 69 | outputEncoding: 'gbk', 70 | compress: 0 71 | }, function(e, report){ 72 | report[0].imports.length.should.equal(8); 73 | done(); 74 | }); 75 | }); 76 | 77 | }); 78 | 79 | describe('When remove charset', function() { 80 | it('should get proper result', function() { 81 | var removeCharset = ComboCore.prototype.removeCharset; 82 | 83 | removeCharset('@charset "utf-8";samp{font-family:"monospace";font-size:.1rem}').should.equal('samp{font-family:"monospace";font-size:.1rem}'); 84 | removeCharset("@charset 'utf-8'; div { width: 0; }").should.equal(' div { width: 0; }'); 85 | removeCharset("@charset 'gbk'; div { width: 0; }").should.equal(' div { width: 0; }'); 86 | removeCharset("@charset 'gb2312' ; div { width: 0; }").should.equal(' div { width: 0; }'); 87 | removeCharset("@charset 'utf-8';").should.equal(''); 88 | }); 89 | }); 90 | 91 | describe('When build ', function(){ 92 | 93 | it('should have no errors.', function(done){ 94 | CssCombo.build({ 95 | target: path.resolve(__dirname, 'css/test.source.css'), 96 | debug: false, 97 | paths: [ path.resolve(__dirname, 'css/external' ) ], 98 | inputEncoding: 'gbk', 99 | outputEncoding: 'gbk', 100 | output:path.resolve(__dirname, 'css/test.combo.css'), 101 | compress: 0 102 | }, function(e, report){ 103 | if(e){ 104 | throw new Error(e); 105 | }else{ 106 | report = report[0]; 107 | if (report.target !== path.resolve(__dirname, 'css/test.source.css')) { 108 | throw new Error('report.target Error'); 109 | } 110 | if (report.output !== path.resolve(__dirname, 'css/test.combo.css')) { 111 | throw new Error('report.output Error'); 112 | } 113 | if (report.imports.length !== 8) { 114 | throw new Error('report.imports Error'); 115 | } 116 | } 117 | done(); 118 | }); 119 | }); 120 | 121 | it('should not replace data uris', function(done){ 122 | CssCombo.build({ 123 | target: path.resolve(__dirname, 'css/test3.source.css'), 124 | debug: false, 125 | inputEncoding: 'utf-8', 126 | outputEncoding: 'utf-8', 127 | output:path.resolve(__dirname, 'css/test3.combo.css'), 128 | compress: 0 129 | }, function(e, report){ 130 | if(e){ 131 | throw new Error(e); 132 | }else{ 133 | report = report[0]; 134 | if (report.target !== path.resolve(__dirname, 'css/test3.source.css')) { 135 | throw new Error('report.target Error'); 136 | } 137 | if (report.output !== path.resolve(__dirname, 'css/test3.combo.css')) { 138 | throw new Error('report.output Error'); 139 | } 140 | if (report.imports.length !== 1) { 141 | throw new Error('report.imports Error'); 142 | } 143 | } 144 | done(); 145 | }); 146 | }); 147 | 148 | //a2u ZH test 149 | it('should replace ZH text to css unicode',function(done){ 150 | CssCombo.build({ 151 | target: path.resolve(__dirname, 'css/a2u.source.css'), 152 | debug: false, 153 | inputEncoding: 'utf-8', 154 | outputEncoding: 'utf-8', 155 | output:path.resolve(__dirname, 'css/a2u.combo.css'), 156 | compress: 0 157 | },function(e,report){ 158 | if(e){ 159 | throw new Error(e); 160 | }else{ 161 | report = report[0]; 162 | if(report.content.match(/[\u4E00-\u9FA5\uF900-\uFA2D]/)){ 163 | throw new Error('report.content Error'); 164 | } 165 | } 166 | done(); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('When compress,', function(){ 172 | var compressor = require('../lib/cssmin').compressor; 173 | it('should preserve pseudo-class colons', function(){ 174 | var text = 'a :nth-child(2), a :nth-child(5), a :nth-child(9) {display: none;}'; 175 | compressor.cssmin(text).should.equal('a :nth-child(2),a :nth-child(5),a :nth-child(9){display:none}'); 176 | }); 177 | }); -------------------------------------------------------------------------------- /lib/combo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: daxingplay 3 | * @date: 1/27/15 4 | */ 5 | 6 | "use strict"; 7 | 8 | var path = require('path'); 9 | var fs = require('fs'); 10 | var utils = require('./utils'); 11 | var _ = require('lodash'); 12 | var compressor = require('./cssmin').compressor; 13 | var iconv = require('iconv-lite'); 14 | var os = require('os'); 15 | 16 | function CssCombo(cfg){ 17 | var self = this; 18 | 19 | self.imports = []; 20 | /** 21 | * 额外进行查找的路径,如:paths: 'path/to/app/utils', path/to/app/public/a.js文件内容:@import "mod.css" 22 | * 1、现在当前路径下寻找: path/to/app/public/mod.css 23 | * 2、若找不到,则再查找: path/to/app/utils/mod.css 24 | */ 25 | self.paths = cfg.paths || []; 26 | utils.debug = cfg.debug; 27 | cfg.target = cfg.src || cfg.target; 28 | cfg.native2ascii = cfg.native2ascii !== false; 29 | cfg.replaceFont = cfg.replaceFont !== false; 30 | if(!cfg.target) { 31 | throw new Error('Please enter css path'); 32 | }else{ 33 | cfg.target = cfg.src = path.resolve(cfg.target); 34 | 35 | if(!cfg.outputEncoding || cfg.outputEncoding == 'gbk' || cfg.outputEncoding == 'GBK' || cfg.outputEncoding == 'gb2312') { 36 | cfg.outputEncoding = ''; 37 | } 38 | 39 | cfg.linefeed = cfg.linefeed || os.EOL; 40 | 41 | cfg.compress = !!cfg.compress; 42 | 43 | cfg.silent = !!cfg.silent; 44 | 45 | cfg.dest = cfg.output = (cfg.dest || cfg.output) ? path.resolve(cfg.dest) : ''; 46 | 47 | cfg.suffix = cfg.suffix || '.combo'; 48 | 49 | cfg.sourceDir = path.dirname(cfg.target); 50 | 51 | self.config = cfg; 52 | //self.build(callback); 53 | } 54 | } 55 | 56 | CssCombo.prototype = { 57 | error: function(e){ 58 | var self = this; 59 | return { 60 | type: e.type || 'Syntax', 61 | message: e.message, 62 | filename: path.basename(self.config.target), 63 | filepath: self.config.target 64 | }; 65 | }, 66 | isExcluded: function(filename){ 67 | var config = this.config; 68 | for(var i in config.exclude){ 69 | if(config.exclude[i].test(filename)){ 70 | return true; 71 | } 72 | } 73 | return false; 74 | }, 75 | isRemoteFile: function(filepath){ 76 | return (/^http(s?):\/\//).test(filepath) || /^\/\//.test(filepath); 77 | }, 78 | getFileContent: function(file, callback){ 79 | var self = this, 80 | config = self.config, 81 | content = ''; 82 | if(!self.isRemoteFile(file)){ 83 | if(!self.isExcluded(file)){ 84 | var filePath = path.resolve(config.base, file); 85 | if(fs.existsSync(filePath)){ 86 | var buf = fs.readFileSync(filePath); 87 | content = iconv.decode(buf, config.inputEncoding ? config.inputEncoding : utils.detectCharset(buf)); 88 | }else{ 89 | // 若在当前文件夹找不到,就去paths中寻找,直到找到为止 90 | self.paths.forEach(function( pathBase ){ 91 | if( !content ){ 92 | var outFilePath = path.resolve( pathBase, file); 93 | if( fs.existsSync(outFilePath) ){ 94 | var buf = fs.readFileSync( outFilePath ); 95 | content = iconv.decode(buf, config.inputEncoding ? config.inputEncoding : utils.detectCharset(buf)); 96 | } 97 | } 98 | }); 99 | 100 | if(!content){ 101 | utils.log('cannot find file ' + filePath, 'warning'); 102 | } 103 | } 104 | }else{ 105 | utils.log('file excluded: ' + file, 'debug'); 106 | } 107 | callback && callback(content); 108 | }else{ 109 | utils.log('Try to get remote file: ' + file, 'debug'); 110 | utils.getRemoteFile(file, function(data, charset){ 111 | content = iconv.decode(data, charset); 112 | callback && callback(content); 113 | }); 114 | } 115 | }, 116 | generateOutput: function(fileContent){ 117 | var self = this, 118 | config = self.config, 119 | commentTpl = [ 120 | '/*' + config.linefeed, 121 | 'combined files : ' + config.linefeed, 122 | config.linefeed 123 | ]; 124 | 125 | for (var i in self.imports) { 126 | commentTpl.push(self.imports[i] + config.linefeed); 127 | } 128 | 129 | commentTpl.push('*/' + config.linefeed); 130 | 131 | // join combo comment to file content. 132 | fileContent = commentTpl.join('') + config.linefeed + fileContent; 133 | var cssFileExt = config.suffix + '.css'; 134 | if(config.compress){ 135 | utils.log('start compress file.', 'debug'); 136 | fileContent = compressor.cssmin(fileContent); 137 | cssFileExt = '.css'; 138 | } 139 | fileContent = iconv.encode(fileContent, config.outputEncoding || 'gbk'); 140 | var comboFile = config.output; 141 | // if output is not a file name, then generate a file name with .combo.css or .css. 142 | if(path.extname(config.output) !== '.css'){ 143 | comboFile = path.resolve(config.output, path.basename(config.target).replace(/(\.source)?\.css/, cssFileExt)); 144 | utils.log('dest is dir, I guess the output file is: ' + comboFile, 'debug'); 145 | } 146 | 147 | utils.log('start generate combo file: ' + comboFile, 'debug'); 148 | 149 | utils.mkdirSync(path.dirname(comboFile)); 150 | 151 | // fs.writeFileSync(comboFile, fileContent, ''); 152 | // if exists, unlink first, otherwise, there may be some problems with the file encoding. 153 | if(fs.existsSync(comboFile)){ 154 | fs.unlinkSync(comboFile); 155 | } 156 | 157 | // write file 158 | var fd = fs.openSync(comboFile, 'w'); 159 | fs.writeSync(fd, fileContent, 0, fileContent.length); 160 | fs.closeSync(fd); 161 | 162 | utils.log('Successfully generated combo file: ' + comboFile, 'debug'); 163 | }, 164 | // 从内容中提取import列表 165 | extractImports: function(content) { 166 | var reg = /@import\s*(url)?\s*[\('"]+([^'"]+)\.css(\?[^\s]*)?\s*['"\)]+\s*[^;]*;/ig; 167 | var result = reg.exec(content); 168 | if (result && result[2]) { 169 | return { 170 | match: result[0], 171 | filePath: result[2] + '.css' 172 | }; 173 | } 174 | return null; 175 | }, 176 | analyzeImports: function(content, callback){ 177 | var self = this; 178 | // utils.log('Analyzing ' + self.config.target, 'debug'); 179 | if(content){ 180 | var result = self.extractImports(content); 181 | if(result !== null){ 182 | var filePath = result.filePath; 183 | self.imports.push(filePath); 184 | self.getFileContent(filePath, function(data){ 185 | data = self.modifySubImportsPath(data,filePath); 186 | data = data.replace(/(['"]|\():/g,'$1'); 187 | data = data.replace(/(['"])%/g,'$1'); 188 | content = content.replace(result.match, '\n' + data + '\n'); 189 | content = self.analyzeImports(content, callback); 190 | }); 191 | }else{ 192 | callback && callback(content); 193 | } 194 | }else{ 195 | utils.log('content empty.', 'debug'); 196 | callback && callback(content); 197 | } 198 | }, 199 | // 修改子文件import的相对路径 200 | // 子文件夹中所有的相对路径都要进行转换 201 | // 先看一下什么形式的地址需要转换 202 | // @import "./mods/import.css" 203 | // background: url("./mods/import.png"); 204 | // list-style: url("./mods/import.jpg"); 205 | // @font-face { 206 | // src:url("./mods/import.ttf"); 207 | // } 208 | modifySubImportsPath: function(content,filePath) { 209 | 'use strict'; 210 | 211 | var self = this; 212 | 213 | var regImport = /@import\s*(url)?\(?['"]([^'"%]+)\.css['"]\)?[^;]*;/ig, 214 | regImageOrFont = /(url)?\(['"]?([^:\)]+\.(png|jpg|gif|jpeg|ttf|eot|woff|svg|webp))([^\)]*)['"]?\)/ig, 215 | importResult, 216 | picAndFontResult; 217 | 218 | var importFilePath = path.dirname(path.resolve(self.config.sourceDir,filePath)); 219 | 220 | // 替换import 221 | importResult = regImport.exec(content); 222 | if (typeof importResult !== 'undefined' && importResult && importResult[2]) { 223 | var importAbsoluteUrl = path.resolve(importFilePath,importResult[2]); 224 | // 用%号表示已经替换好的import路径,后续会再去掉百分号,这里替换的时 225 | // 候要注意全局的替换 226 | var regimportReplace = new RegExp(importResult[2],'g'); 227 | content = content.replace(regimportReplace, "%" + path.relative(self.config.sourceDir,importAbsoluteUrl)); 228 | return self.modifySubImportsPath(content, filePath); 229 | } 230 | // 替换图片和font的路径 231 | picAndFontResult = regImageOrFont.exec(content); 232 | if (typeof picAndFontResult !== 'undefined' && picAndFontResult && picAndFontResult[2] && !/^\/\/[^\/]+/.test(picAndFontResult[2])) { 233 | var regpicReplace = new RegExp(picAndFontResult[2],'g'); 234 | var picAbsolutePath = path.resolve(importFilePath,picAndFontResult[2]); 235 | //解决win平台下路径的斜杠问题 236 | var isWin = (process.platform === 'win32'); 237 | var _path = path.relative(self.config.sourceDir,picAbsolutePath); 238 | if(isWin){ 239 | _path = path.relative(self.config.sourceDir,picAbsolutePath).split(path.sep).join("\/"); 240 | } 241 | // 用:号表示已经替换好的import路径,后续会再去掉冒号 242 | content = content.replace(regpicReplace, ":" + _path); 243 | return self.modifySubImportsPath(content, filePath); 244 | } 245 | return content; 246 | }, 247 | removeCharset: function(str) { 248 | return (str || '').replace(/@charset\s+['|"][^;"']+?["|']\s*;/g, ''); 249 | }, 250 | analyze: function(callback){ 251 | var self = this, 252 | config = self.config, 253 | file = config.target, 254 | report = {}; 255 | 256 | utils.log('start analyze file : ' + file, 'debug'); 257 | 258 | config.base = path.dirname(file); 259 | 260 | var data = fs.readFileSync(file); 261 | utils.log('Cur file is ' + self.config.target, 'debug'); 262 | utils.log('file read: ' + file, 'debug'); 263 | 264 | config.inputEncoding = config.inputEncoding ? config.inputEncoding : utils.detectCharset(data); 265 | var fileContent = iconv.decode(data, config.inputEncoding); 266 | utils.log('file charset is: ' + config.inputEncoding, 'debug'); 267 | 268 | // preserve data url and comment. 269 | var preservedTokens = []; 270 | // fileContent = compressor._extractDataUrls(fileContent, preservedTokens); 271 | fileContent = compressor._extractComments(fileContent, preservedTokens); 272 | 273 | // start analyze file content 274 | self.analyzeImports(fileContent, function(data){ 275 | utils.log('analyze done.', 'debug'); 276 | // after combo, @charset position may be changed. since the output file encoding is specified, we should remove @charset. 277 | data = self.removeCharset(data); 278 | // restore all comments back. 279 | data = compressor._restoreComments(data, preservedTokens); 280 | //convert ZH to unicode 281 | if(config.native2ascii === true){ 282 | data = data.replace(/[\u4E00-\u9FA5\uF900-\uFA2D]/g,function($1){ 283 | return '\\'+utils.a2u($1).replace('%u',''); 284 | }); 285 | } 286 | //convert unicode font-family to EN 287 | if(config.replaceFont === true){ 288 | data = data.replace(/[\'|\"](\\\w{4}.*)[\'|\"]/g,function($1,$2){ 289 | $2 = $2.trim(); 290 | var en = utils.unicode2En($2), 291 | temp = ''; 292 | if(_.isArray(en)){ 293 | for(var i = 0;i 0) && (css.charAt(endIndex - 1) !== '\\')) { 71 | foundTerminator = true; 72 | if (")" != terminator) { 73 | endIndex = css.indexOf(")", endIndex); 74 | } 75 | } 76 | } 77 | 78 | // Enough searching, start moving stuff over to the buffer 79 | sb.push(css.substring(appendIndex, m.index)); 80 | 81 | if (foundTerminator) { 82 | token = css.substring(startIndex, endIndex); 83 | token = token.replace(/\s+/g, ""); 84 | preservedTokens.push(token); 85 | 86 | preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)"; 87 | sb.push(preserver); 88 | 89 | appendIndex = endIndex + 1; 90 | } else { 91 | // No end terminator found, re-add the whole match. Should we throw/warn here? 92 | sb.push(css.substring(m.index, pattern.lastIndex)); 93 | appendIndex = pattern.lastIndex; 94 | } 95 | } 96 | 97 | sb.push(css.substring(appendIndex)); 98 | 99 | return sb.join(""); 100 | }; 101 | 102 | /** 103 | * Utility method to compress hex color values of the form #AABBCC to #ABC. 104 | * 105 | * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). 106 | * e.g. #AddressForm { ... } 107 | * 108 | * DOES NOT compress IE filters, which have hex color values (which would break things). 109 | * e.g. filter: chroma(color="#FFFFFF"); 110 | * 111 | * DOES NOT compress invalid hex values. 112 | * e.g. background-color: #aabbccdd 113 | * 114 | * @private 115 | * @method _compressHexColors 116 | * @param {String} css The input css 117 | * @returns String The processed css 118 | */ 119 | YAHOO.compressor._compressHexColors = function(css) { 120 | 121 | // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) 122 | var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi, 123 | m, 124 | index = 0, 125 | isFilter, 126 | sb = []; 127 | 128 | while ((m = pattern.exec(css)) !== null) { 129 | 130 | sb.push(css.substring(index, m.index)); 131 | 132 | isFilter = m[1]; 133 | 134 | if (isFilter) { 135 | // Restore, maintain case, otherwise filter will break 136 | sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7])); 137 | } else { 138 | if (m[2].toLowerCase() == m[3].toLowerCase() && 139 | m[4].toLowerCase() == m[5].toLowerCase() && 140 | m[6].toLowerCase() == m[7].toLowerCase()) { 141 | 142 | // Compress. 143 | sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase()); 144 | } else { 145 | // Non compressible color, restore but lower case. 146 | sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase()); 147 | } 148 | } 149 | 150 | index = pattern.lastIndex = pattern.lastIndex - m[8].length; 151 | } 152 | 153 | sb.push(css.substring(index)); 154 | 155 | return sb.join(""); 156 | }; 157 | 158 | YAHOO.compressor.cssmin = function (css, linebreakpos) { 159 | 160 | var startIndex = 0, 161 | endIndex = 0, 162 | i = 0, max = 0, 163 | preservedTokens = [], 164 | comments = [], 165 | token = '', 166 | totallen = css.length, 167 | placeholder = ''; 168 | 169 | css = this._extractDataUrls(css, preservedTokens); 170 | 171 | // collect all comment blocks... 172 | while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { 173 | endIndex = css.indexOf("*/", startIndex + 2); 174 | if (endIndex < 0) { 175 | endIndex = totallen; 176 | } 177 | token = css.slice(startIndex + 2, endIndex); 178 | comments.push(token); 179 | css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); 180 | startIndex += 2; 181 | } 182 | 183 | // preserve strings so their content doesn't get accidentally minified 184 | css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { 185 | var i, max, quote = match.substring(0, 1); 186 | 187 | match = match.slice(1, -1); 188 | 189 | // maybe the string contains a comment-like substring? 190 | // one, maybe more? put'em back then 191 | if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { 192 | for (i = 0, max = comments.length; i < max; i = i + 1) { 193 | match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); 194 | } 195 | } 196 | 197 | // minify alpha opacity in filter strings 198 | match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); 199 | 200 | preservedTokens.push(match); 201 | return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; 202 | }); 203 | 204 | // strings are safe, now wrestle the comments 205 | for (i = 0, max = comments.length; i < max; i = i + 1) { 206 | 207 | token = comments[i]; 208 | placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; 209 | 210 | // ! in the first position of the comment means preserve 211 | // so push to the preserved tokens keeping the ! 212 | if (token.charAt(0) === "!") { 213 | preservedTokens.push(token); 214 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 215 | continue; 216 | } 217 | 218 | // \ in the last position looks like hack for Mac/IE5 219 | // shorten that to /*\*/ and the next one to /**/ 220 | if (token.charAt(token.length - 1) === "\\") { 221 | preservedTokens.push("\\"); 222 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 223 | i = i + 1; // attn: advancing the loop 224 | preservedTokens.push(""); 225 | css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 226 | continue; 227 | } 228 | 229 | // keep empty comments after child selectors (IE7 hack) 230 | // e.g. html >/**/ body 231 | if (token.length === 0) { 232 | startIndex = css.indexOf(placeholder); 233 | if (startIndex > 2) { 234 | if (css.charAt(startIndex - 3) === '>') { 235 | preservedTokens.push(""); 236 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 237 | } 238 | } 239 | } 240 | 241 | // in all other cases kill the comment 242 | css = css.replace("/*" + placeholder + "*/", ""); 243 | } 244 | 245 | 246 | // Normalize all whitespace strings to single spaces. Easier to work with that way. 247 | css = css.replace(/\s+/g, " "); 248 | 249 | // Remove the spaces before the things that should not have spaces before them. 250 | // But, be careful not to turn "p :link {...}" into "p:link{...}" 251 | // Swap out any pseudo-class colons with the token, and then swap back. 252 | css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) { 253 | return m.replace(/:/g, "___YUICSSMIN_PSEUDOCLASSCOLON___"); 254 | }); 255 | css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); 256 | css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); 257 | 258 | // retain space for special IE6 cases 259 | css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2"); 260 | 261 | // no space after the end of a preserved comment 262 | css = css.replace(/\*\/ /g, '*/'); 263 | 264 | 265 | // If there is a @charset, then only allow one, and push to the top of the file. 266 | css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); 267 | css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); 268 | 269 | // Put the space back in some cases, to support stuff like 270 | // @media screen and (-webkit-min-device-pixel-ratio:0){ 271 | css = css.replace(/\band\(/gi, "and ("); 272 | 273 | 274 | // Remove the spaces after the things that should not have spaces after them. 275 | css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); 276 | 277 | // remove unnecessary semicolons 278 | css = css.replace(/;+\}/g, "}"); 279 | 280 | // Replace 0(px,em,%) with 0. 281 | css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); 282 | 283 | // Replace 0 0 0 0; with 0. 284 | css = css.replace(/:0 0 0 0(;|\})/g, ":0$1"); 285 | css = css.replace(/:0 0 0(;|\})/g, ":0$1"); 286 | css = css.replace(/:0 0(;|\})/g, ":0$1"); 287 | 288 | // Replace background-position:0; with background-position:0 0; 289 | // same for transform-origin 290 | css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) { 291 | return prop.toLowerCase() + ":0 0" + tail; 292 | }); 293 | 294 | // Replace 0.6 to .6, but only when preceded by : or a white-space 295 | css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); 296 | 297 | // Shorten colors from rgb(51,102,153) to #336699 298 | // This makes it more likely that it'll get further compressed in the next step. 299 | css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () { 300 | var i, rgbcolors = arguments[1].split(','); 301 | for (i = 0; i < rgbcolors.length; i = i + 1) { 302 | rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); 303 | if (rgbcolors[i].length === 1) { 304 | rgbcolors[i] = '0' + rgbcolors[i]; 305 | } 306 | } 307 | return '#' + rgbcolors.join(''); 308 | }); 309 | 310 | // Shorten colors from #AABBCC to #ABC. 311 | css = this._compressHexColors(css); 312 | 313 | // border: none -> border:0 314 | css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) { 315 | return prop.toLowerCase() + ":0" + tail; 316 | }); 317 | 318 | // shorter opacity IE filter 319 | css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); 320 | 321 | // Remove empty rules. 322 | css = css.replace(/[^\};\{\/]+\{\}/g, ""); 323 | 324 | if (linebreakpos >= 0) { 325 | // Some source control tools don't like it when files containing lines longer 326 | // than, say 8000 characters, are checked in. The linebreak option is used in 327 | // that case to split long lines after a specific column. 328 | startIndex = 0; 329 | i = 0; 330 | while (i < css.length) { 331 | i = i + 1; 332 | if (css[i - 1] === '}' && i - startIndex > linebreakpos) { 333 | css = css.slice(0, i) + '\n' + css.slice(i); 334 | startIndex = i; 335 | } 336 | } 337 | } 338 | 339 | // Replace multiple semi-colons in a row by a single one 340 | // See SF bug #1980989 341 | css = css.replace(/;;+/g, ";"); 342 | 343 | // restore preserved comments and strings 344 | for (i = 0, max = preservedTokens.length; i < max; i = i + 1) { 345 | css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); 346 | } 347 | 348 | // Trim the final string (for any leading or trailing white spaces) 349 | css = css.replace(/^\s+|\s+$/g, ""); 350 | 351 | return css; 352 | 353 | }; 354 | 355 | YAHOO.compressor._extractComments = function(css, preservedTokens){ 356 | var startIndex = 0, 357 | endIndex = 0, 358 | i = 0, 359 | max = 0, 360 | // comments = [], 361 | token = '', 362 | totallen = css.length, 363 | placeholder = ''; 364 | // collect all comment blocks... 365 | while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { 366 | endIndex = css.indexOf("*/", startIndex + 2); 367 | if (endIndex < 0) { 368 | endIndex = totallen; 369 | } 370 | token = css.slice(startIndex + 2, endIndex); 371 | preservedTokens.push(token); 372 | // comments.push(token); 373 | // css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); 374 | css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (preservedTokens.length - 1) + "___" + css.slice(endIndex); 375 | startIndex += 2; 376 | } 377 | 378 | // console.log(css); 379 | // console.log(comments); 380 | 381 | // preserve strings so their content doesn't get accidentally minified 382 | // css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { 383 | // var i, max, quote = match.substring(0, 1); 384 | // 385 | // match = match.slice(1, -1); 386 | // 387 | // // maybe the string contains a comment-like substring? 388 | // // one, maybe more? put'em back then 389 | // if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { 390 | // for (i = 0, max = comments.length; i < max; i = i + 1) { 391 | // match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); 392 | // } 393 | // } 394 | // 395 | // // minify alpha opacity in filter strings 396 | // match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); 397 | // 398 | // preservedTokens.push(match); 399 | // return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; 400 | // }); 401 | // 402 | // console.log(css); 403 | // 404 | // // strings are safe, now wrestle the comments 405 | // for (i = 0, max = comments.length; i < max; i = i + 1) { 406 | // 407 | // token = comments[i]; 408 | // placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; 409 | // 410 | // // ! in the first position of the comment means preserve 411 | // // so push to the preserved tokens keeping the ! 412 | // if (token.charAt(0) === "!") { 413 | // preservedTokens.push(token); 414 | // css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 415 | // continue; 416 | // } 417 | // 418 | // // \ in the last position looks like hack for Mac/IE5 419 | // // shorten that to /*\*/ and the next one to /**/ 420 | // if (token.charAt(token.length - 1) === "\\") { 421 | // preservedTokens.push("\\"); 422 | // css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 423 | // i = i + 1; // attn: advancing the loop 424 | // preservedTokens.push(""); 425 | // css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 426 | // continue; 427 | // } 428 | // 429 | // // keep empty comments after child selectors (IE7 hack) 430 | // // e.g. html >/**/ body 431 | // if (token.length === 0) { 432 | // startIndex = css.indexOf(placeholder); 433 | // if (startIndex > 2) { 434 | // if (css.charAt(startIndex - 3) === '>') { 435 | // preservedTokens.push(""); 436 | // css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 437 | // } 438 | // } 439 | // } 440 | // 441 | // // in all other cases kill the comment 442 | // css = css.replace("/*" + placeholder + "*/", ""); 443 | // } 444 | 445 | return css; 446 | }; 447 | 448 | YAHOO.compressor._restoreComments = function(css, preservedTokens){ 449 | // restore preserved comments and strings 450 | for (var i = 0, max = preservedTokens.length; i < max; i = i + 1) { 451 | css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", preservedTokens[i]); 452 | } 453 | return css; 454 | } 455 | 456 | /** 457 | * used to preserve comments before analyze css content. 458 | * @param str {String} css file content 459 | * @return {String} css file content without comments 460 | */ 461 | //YAHOO.compressor.removeComments = function(str){ 462 | // var totallen = str.length, token; 463 | // 464 | // // ȥ������ע�� 465 | // var startIndex = 0; 466 | // var endIndex = 0; 467 | // var comments = []; // ��¼ע������ 468 | // while ((startIndex = str.indexOf("//", startIndex)) >= 0) { 469 | // endIndex = str.indexOf("\n", startIndex + 2); 470 | // if (endIndex < 0) { 471 | // endIndex = totallen; 472 | // } 473 | // token = str.slice(startIndex + 2, endIndex); 474 | // comments.push(token); 475 | // // str = str.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); 476 | // startIndex += 2; 477 | // } 478 | // for (i=0,max=comments.length; i= 0) { 487 | // endIndex = str.indexOf("*/", startIndex + 2); 488 | // if (endIndex < 0) { 489 | // endIndex = totallen; 490 | // } 491 | // token = str.slice(startIndex + 2, endIndex); 492 | // comments.push(token); 493 | // // str = str.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); 494 | // startIndex += 2; 495 | // } 496 | // for (i=0,max=comments.length; i