├── .editorconfig ├── .gitignore ├── README.md ├── bin └── echarts-optimize ├── conf.js ├── index.js ├── lib ├── amd.js ├── analyse.js ├── etpl.js ├── read-module-file.js └── read-resource-file.js ├── package.json └── resource ├── all-end.js ├── all-nut.js ├── all-start.js └── esl.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [**.js] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [**.css] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [**.less] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [**.styl] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [**.html] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [**.tpl] 30 | indent_style = space 31 | indent_size = 4 32 | 33 | [**.json] 34 | indent_style = space 35 | indent_size = 4 36 | 37 | [*.md] 38 | trim_trailing_whitespace = false 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.swp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project has been deprecated 2 | -------------------------------------------------------------------------------- /bin/echarts-optimize: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | // parse args 4 | var program = require('commander'); 5 | program 6 | .usage('[output-directory] [--debug] [-c configuration-file]') 7 | .option('--debug', 'Output dependencies analyse result to dist dir.') 8 | .option('-c --config ', 'Configuration file path, default ${pwd}/echarts-optimize-conf.js') 9 | .option('-o --output ', 'Output directory. You can also specify output directory by arg.') 10 | .parse(process.argv); 11 | 12 | var fs = require('fs'); 13 | var index = require('../index'); 14 | 15 | var confFile = require('path').join( 16 | process.cwd(), 17 | program.config || 'echarts-optimize-conf.js' 18 | ); 19 | 20 | if (fs.existsSync(confFile)) { 21 | console.log('Use custom configuration file: ' + confFile); 22 | index.setConf(require(confFile)); 23 | } 24 | else { 25 | index.setConf(); 26 | } 27 | 28 | 29 | index.setDistDir(program.output || program.args[0]); 30 | console.log('Output to directory: ' + index.getDistDir()); 31 | 32 | index.analyse(program.debug); 33 | index.packAsAll(); 34 | index.packAsDemand(); 35 | 36 | -------------------------------------------------------------------------------- /conf.js: -------------------------------------------------------------------------------- 1 | exports.modules = { 2 | main: {name: 'echarts/echarts'}, 3 | parts: [ 4 | {name: 'echarts/chart/line', weight: 100}, 5 | {name: 'echarts/chart/bar', weight: 100}, 6 | {name: 'echarts/chart/scatter', weight: 90}, 7 | {name: 'echarts/chart/k', weight: 30}, 8 | {name: 'echarts/chart/pie', weight: 90}, 9 | {name: 'echarts/chart/radar', weight: 30}, 10 | {name: 'echarts/chart/chord', weight: 30}, 11 | {name: 'echarts/chart/force', weight: 30}, 12 | { 13 | name: 'echarts/chart/map', 14 | weight: 90, 15 | includeShallow: [ 16 | 'echarts/util/mapData/geoJson/an_hui_geo', 17 | 'echarts/util/mapData/geoJson/ao_men_geo', 18 | 'echarts/util/mapData/geoJson/bei_jing_geo', 19 | 'echarts/util/mapData/geoJson/china_geo', 20 | 'echarts/util/mapData/geoJson/chong_qing_geo', 21 | 'echarts/util/mapData/geoJson/fu_jian_geo', 22 | 'echarts/util/mapData/geoJson/gan_su_geo', 23 | 'echarts/util/mapData/geoJson/guang_dong_geo', 24 | 'echarts/util/mapData/geoJson/guang_xi_geo', 25 | 'echarts/util/mapData/geoJson/gui_zhou_geo', 26 | 'echarts/util/mapData/geoJson/hai_nan_geo', 27 | 'echarts/util/mapData/geoJson/hei_long_jiang_geo', 28 | 'echarts/util/mapData/geoJson/he_bei_geo', 29 | 'echarts/util/mapData/geoJson/he_nan_geo', 30 | 'echarts/util/mapData/geoJson/hu_bei_geo', 31 | 'echarts/util/mapData/geoJson/hu_nan_geo', 32 | 'echarts/util/mapData/geoJson/jiang_su_geo', 33 | 'echarts/util/mapData/geoJson/jiang_xi_geo', 34 | 'echarts/util/mapData/geoJson/ji_lin_geo', 35 | 'echarts/util/mapData/geoJson/liao_ning_geo', 36 | 'echarts/util/mapData/geoJson/nei_meng_gu_geo', 37 | 'echarts/util/mapData/geoJson/ning_xia_geo', 38 | 'echarts/util/mapData/geoJson/qing_hai_geo', 39 | 'echarts/util/mapData/geoJson/shang_hai_geo', 40 | 'echarts/util/mapData/geoJson/shan_dong_geo', 41 | 'echarts/util/mapData/geoJson/shan_xi_1_geo', 42 | 'echarts/util/mapData/geoJson/shan_xi_2_geo', 43 | 'echarts/util/mapData/geoJson/si_chuan_geo', 44 | 'echarts/util/mapData/geoJson/tai_wan_geo', 45 | 'echarts/util/mapData/geoJson/tian_jin_geo', 46 | 'echarts/util/mapData/geoJson/world_geo', 47 | 'echarts/util/mapData/geoJson/xiang_gang_geo', 48 | 'echarts/util/mapData/geoJson/xin_jiang_geo', 49 | 'echarts/util/mapData/geoJson/xi_zang_geo', 50 | 'echarts/util/mapData/geoJson/yun_nan_geo', 51 | 'echarts/util/mapData/geoJson/zhe_jiang_geo' 52 | ] 53 | }, 54 | {name: 'echarts/chart/gauge', weight: 30}, 55 | {name: 'echarts/chart/funnel', weight: 30}, 56 | {name: 'echarts/chart/eventRiver', weight: 10} 57 | ] 58 | }; 59 | 60 | exports.amd = { 61 | baseUrl: process.cwd(), 62 | packages: [ 63 | { 64 | name: 'echarts', 65 | location: 'echarts/src', 66 | main: 'echarts' 67 | }, 68 | { 69 | name: 'zrender', 70 | location: 'zrender/src', 71 | main: 'zrender' 72 | } 73 | ] 74 | }; 75 | 76 | exports.name = 'echarts'; 77 | exports.includeEsl = true; 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var conf; 5 | var modules; 6 | 7 | function readResourceFile(fileName) { 8 | return fs.readFileSync( 9 | path.join(__dirname, 'resource', fileName), 10 | 'UTF-8' 11 | ); 12 | } 13 | var eslCode = readResourceFile('esl.js'); 14 | var wrapStart = readResourceFile('all-start.js'); 15 | var wrapNut = readResourceFile('all-nut.js'); 16 | var wrapEndTpl = readResourceFile('all-end.js'); 17 | 18 | var HIGH_WEIGHT = 100; 19 | var BUILTIN_MODULES = ['require', 'module', 'exports']; 20 | 21 | var amd = require('./lib/amd'); 22 | var analyse = require('./lib/analyse'); 23 | var etpl = require('./lib/etpl'); 24 | etpl.config({ 25 | commandOpen: '/**', 26 | commandClose: '*/' 27 | }); 28 | 29 | exports.setConf = function (customConf) { 30 | conf = customConf || require('./conf'); 31 | modules = conf.modules; 32 | amd.config(conf.amd); 33 | }; 34 | 35 | var distDir = path.join(process.cwd(), 'dist'); 36 | exports.setDistDir = function (dir) { 37 | if (dir) { 38 | distDir = dir; 39 | } 40 | }; 41 | 42 | exports.getDistDir = function () { 43 | return distDir; 44 | }; 45 | 46 | function writeFile(file, content) { 47 | var filePath = path.join(distDir, file); 48 | require('mkdirp').sync(path.dirname(filePath)); 49 | fs.writeFileSync(filePath, content, 'UTF-8'); 50 | } 51 | 52 | 53 | exports.analyse = function (debug) { 54 | var main = modules.main; 55 | 56 | // analyse dependencies 57 | main.dependencies = subtract(analyse(main.name, 1, {}, main.exclude), BUILTIN_MODULES); 58 | debug && writeFile('analyses/echarts.dependencies', main.dependencies.join('\n')); 59 | modules.parts.forEach(function (mod) { 60 | mod.dependencies = subtract(analyse(mod.name, 1, {}, mod.exclude), BUILTIN_MODULES); 61 | debug && writeFile( 62 | 'analyses/' + mod.name.split('/').join('.') + '.dependencies', 63 | mod.dependencies.join('\n') 64 | ); 65 | }); 66 | 67 | // analyse extra dependencies of main module 68 | var extraMainDependencies; 69 | modules.parts.forEach(function (mod) { 70 | if (mod.weight >= HIGH_WEIGHT) { 71 | if (!extraMainDependencies) { 72 | extraMainDependencies = mod.dependencies; 73 | } 74 | else { 75 | extraMainDependencies = intersect(extraMainDependencies, mod.dependencies); 76 | } 77 | } 78 | }); 79 | 80 | extraMainDependencies = extraMainDependencies || []; 81 | 82 | // analyse expect dependencies for all modules 83 | main.expectDependencies = union(main.dependencies, extraMainDependencies); 84 | debug && writeFile('analyses/echarts.dependencies.expect', main.expectDependencies.join('\n')); 85 | modules.parts.forEach(function (mod) { 86 | mod.expectDependencies = subtract(mod.dependencies, main.expectDependencies); 87 | 88 | if (mod.includeShallow) { 89 | Array.prototype.push.apply(mod.expectDependencies, mod.includeShallow); 90 | } 91 | debug && writeFile( 92 | 'analyses/' + mod.name.split('/').join('.') + '.dependencies.expect', 93 | mod.expectDependencies.join('\n') 94 | ); 95 | }); 96 | }; 97 | 98 | 99 | exports.packAsDemand = function () { 100 | var main = modules.main; 101 | 102 | var includeEsl = conf.includeEsl == null ? true : conf.includeEsl; 103 | // write built source 104 | writeCompiledCode( 105 | (conf.name || 'echarts') + '.js', main.name, main.expectDependencies, 106 | includeEsl ? eslCode : '' 107 | ); 108 | modules.parts.forEach(function (mod) { 109 | writeCompiledCode( 110 | mod.name.slice(mod.name.indexOf('/') + 1) + '.js', 111 | mod.name, 112 | mod.expectDependencies 113 | ); 114 | }); 115 | }; 116 | 117 | exports.packAsAll = function () { 118 | var main = modules.main; 119 | 120 | // calc modules list 121 | var mods = [main.name]; 122 | mods = union(mods, main.expectDependencies); 123 | modules.parts.forEach(function (mod) { 124 | mods = union(mods, [mod.name]); 125 | mods = union(mods, mod.expectDependencies); 126 | }); 127 | 128 | // combine built code 129 | var result = '' 130 | mods.forEach(function (mod) { 131 | result += analyse.getAnalysed(mod).builtCode; 132 | }); 133 | 134 | // write file by wrapped code 135 | var hasMap = modules.parts.filter(function (mod) { 136 | return mod.name.indexOf('chart/map') >= 0; 137 | }).length > 0; 138 | var wrapEnd = etpl.compile(wrapEndTpl)({ 139 | parts: modules.parts, 140 | hasMap: hasMap 141 | }); 142 | var code = wrapStart + wrapNut + result + wrapEnd; 143 | var name = (conf.name || 'echarts') + '-all'; 144 | writeFile( 145 | 'source/' + name + '.js', code 146 | ); 147 | writeFile( 148 | 'dist/' + name + '.js', 149 | jsCompress(code) 150 | ); 151 | }; 152 | 153 | function jsCompress(source) { 154 | var UglifyJS = require('uglify-js'); 155 | var ast = UglifyJS.parse(source); 156 | /* jshint camelcase: false */ 157 | // compressor needs figure_out_scope too 158 | ast.figure_out_scope(); 159 | ast = ast.transform(UglifyJS.Compressor( {} )); 160 | 161 | // need to figure out scope again so mangler works optimally 162 | ast.figure_out_scope(); 163 | ast.compute_char_frequency(); 164 | ast.mangle_names(); 165 | 166 | return ast.print_to_string(); 167 | } 168 | 169 | function writeCompiledCode(file, moduleId, expectDependencies, beforeContent) { 170 | var result = beforeContent || ''; 171 | result += analyse.getAnalysed(moduleId).builtCode; 172 | 173 | expectDependencies.forEach(function (dep) { 174 | var depInfo = analyse.getAnalysed(dep); 175 | if (depInfo) { 176 | result += depInfo.builtCode; 177 | } 178 | }); 179 | 180 | writeFile('source/' + file, result); 181 | writeFile('dist/' + file, jsCompress(result)); 182 | } 183 | 184 | /** 185 | * 并集 186 | * 187 | * @return {Array} 188 | */ 189 | function union(a, b) { 190 | var exists = {}; 191 | var result = []; 192 | 193 | a.forEach(function (item) { 194 | if (!exists[item]) { 195 | exists[item] = 1; 196 | result.push(item); 197 | } 198 | }); 199 | 200 | b.forEach(function (item) { 201 | if (!exists[item]) { 202 | exists[item] = 1; 203 | result.push(item); 204 | } 205 | }); 206 | 207 | return result; 208 | } 209 | 210 | /** 211 | * 交集 212 | * 213 | * @return {Array} 214 | */ 215 | function intersect(a, b) { 216 | var index = {}; 217 | var result = []; 218 | var exists = {}; 219 | 220 | a.forEach(function (item) { 221 | index[item] = 1; 222 | }); 223 | 224 | b.forEach(function (item) { 225 | if (index[item] && !exists[item]) { 226 | result.push(item); 227 | exists[item] = 1; 228 | } 229 | }); 230 | 231 | return result; 232 | } 233 | 234 | /** 235 | * 差集 236 | * 237 | * @return {Array} 238 | */ 239 | function subtract(a, b) { 240 | var index = {}; 241 | b.forEach(function (item) { 242 | index[item] = 1; 243 | }); 244 | 245 | var result = []; 246 | var exists = {}; 247 | a.forEach(function (item) { 248 | if (!index[item] && !exists[item]) { 249 | result.push(item); 250 | exists[item] = 1; 251 | } 252 | }); 253 | 254 | return result; 255 | } 256 | -------------------------------------------------------------------------------- /lib/amd.js: -------------------------------------------------------------------------------- 1 | 2 | var requireConf; 3 | 4 | /** 5 | * paths内部索引 6 | * 7 | * @inner 8 | * @type {Array} 9 | */ 10 | var pathsIndex; 11 | 12 | /** 13 | * packages内部索引 14 | * 15 | * @inner 16 | * @type {Array} 17 | */ 18 | var packagesIndex; 19 | 20 | /** 21 | * mapping内部索引 22 | * 23 | * @inner 24 | * @type {Array} 25 | */ 26 | var mappingIdIndex; 27 | 28 | /** 29 | * 将key为module id prefix的Object,生成数组形式的索引,并按照长度和字面排序 30 | * 31 | * @inner 32 | * @param {Object} value 源值 33 | * @param {boolean} allowAsterisk 是否允许*号表示匹配所有 34 | * @return {Array} 35 | */ 36 | function createKVSortedIndex( value, allowAsterisk ) { 37 | var index = kv2List( value, 1, allowAsterisk ); 38 | index.sort( descSorterByKOrName ); 39 | return index; 40 | } 41 | 42 | /** 43 | * 创建配置信息内部索引 44 | * 45 | * @inner 46 | */ 47 | function createConfIndex() { 48 | requireConf.baseUrl = requireConf.baseUrl.replace( /\/$/, '' ) + '/'; 49 | 50 | // create paths index 51 | pathsIndex = createKVSortedIndex( requireConf.paths ); 52 | 53 | // create mappingId index 54 | mappingIdIndex = createKVSortedIndex( requireConf.map, 1 ); 55 | each( 56 | mappingIdIndex, 57 | function ( item ) { 58 | item.v = createKVSortedIndex( item.v ); 59 | } 60 | ); 61 | 62 | // create packages index 63 | packagesIndex = []; 64 | each( 65 | requireConf.packages, 66 | function ( packageConf ) { 67 | var pkg = packageConf; 68 | if ( typeof packageConf === 'string' ) { 69 | pkg = { 70 | name: packageConf.split('/')[ 0 ], 71 | location: packageConf, 72 | main: 'main' 73 | }; 74 | } 75 | 76 | pkg.location = pkg.location || pkg.name; 77 | pkg.main = (pkg.main || 'main').replace(/\.js$/i, ''); 78 | pkg.reg = createPrefixRegexp( pkg.name ); 79 | packagesIndex.push( pkg ); 80 | } 81 | ); 82 | packagesIndex.sort( descSorterByKOrName ); 83 | } 84 | 85 | /** 86 | * 对配置信息的索引进行检索 87 | * 88 | * @inner 89 | * @param {string} value 要检索的值 90 | * @param {Array} index 索引对象 91 | * @param {Function} hitBehavior 索引命中的行为函数 92 | */ 93 | function indexRetrieve( value, index, hitBehavior ) { 94 | each( index, function ( item ) { 95 | if ( item.reg.test( value ) ) { 96 | hitBehavior( item.v, item.k, item ); 97 | return false; 98 | } 99 | } ); 100 | } 101 | 102 | /** 103 | * 将对象数据转换成数组,数组每项是带有k和v的Object 104 | * 105 | * @inner 106 | * @param {Object} source 对象数据 107 | * @return {Array.} 108 | */ 109 | function kv2List( source, keyMatchable, allowAsterisk ) { 110 | var list = []; 111 | for ( var key in source ) { 112 | if ( source.hasOwnProperty( key ) ) { 113 | var item = { 114 | k: key, 115 | v: source[ key ] 116 | }; 117 | list.push( item ); 118 | 119 | if ( keyMatchable ) { 120 | item.reg = key === '*' && allowAsterisk 121 | ? /^/ 122 | : createPrefixRegexp( key ); 123 | } 124 | } 125 | } 126 | 127 | return list; 128 | } 129 | 130 | /** 131 | * 创建id前缀匹配的正则对象 132 | * 133 | * @inner 134 | * @param {string} prefix id前缀 135 | * @return {RegExp} 136 | */ 137 | function createPrefixRegexp( prefix ) { 138 | return new RegExp( '^' + prefix + '(/|$)' ); 139 | } 140 | 141 | /** 142 | * 循环遍历数组集合 143 | * 144 | * @inner 145 | * @param {Array} source 数组源 146 | * @param {function(Array,Number):boolean} iterator 遍历函数 147 | */ 148 | function each( source, iterator ) { 149 | if ( source instanceof Array ) { 150 | for ( var i = 0, len = source.length; i < len; i++ ) { 151 | if ( iterator( source[ i ], i ) === false ) { 152 | break; 153 | } 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * 根据元素的k或name项进行数组字符数逆序的排序函数 160 | * 161 | * @inner 162 | */ 163 | function descSorterByKOrName( a, b ) { 164 | var aValue = a.k || a.name; 165 | var bValue = b.k || b.name; 166 | 167 | if ( bValue === '*' ) { 168 | return -1; 169 | } 170 | 171 | if ( aValue === '*' ) { 172 | return 1; 173 | } 174 | 175 | return bValue.length - aValue.length; 176 | } 177 | 178 | /** 179 | * 将`模块标识+'.extension'`形式的字符串转换成相对的url 180 | * 181 | * @inner 182 | * @param {string} source 源字符串 183 | * @return {string} 184 | */ 185 | function toUrl( source ) { 186 | // 分离 模块标识 和 .extension 187 | var extReg = /(\.[a-z0-9]+)$/i; 188 | var queryReg = /(\?[^#]*)$/; 189 | var extname = ''; 190 | var id = source; 191 | var query = ''; 192 | 193 | if ( queryReg.test( source ) ) { 194 | query = RegExp.$1; 195 | source = source.replace( queryReg, '' ); 196 | } 197 | 198 | if ( extReg.test( source ) ) { 199 | extname = RegExp.$1; 200 | id = source.replace( extReg, '' ); 201 | } 202 | 203 | var url = id; 204 | 205 | // paths处理和匹配 206 | var isPathMap; 207 | indexRetrieve( id, pathsIndex, function ( value, key ) { 208 | url = url.replace( key, value ); 209 | isPathMap = 1; 210 | } ); 211 | 212 | var path = require('path'); 213 | 214 | // packages处理和匹配 215 | if ( !isPathMap ) { 216 | indexRetrieve( 217 | id, 218 | packagesIndex, 219 | function ( value, key, item ) { 220 | url = path.join(item.location, url.replace( item.name, '' )); 221 | } 222 | ); 223 | } 224 | 225 | // 相对路径时,附加baseUrl 226 | if ( !/^([a-z]{1,10}:\/)?\//i.test( url ) && !/^[a-z]:\\/i.test( url ) ) { 227 | url = path.join(requireConf.baseUrl, url); 228 | } 229 | 230 | // 附加 .extension 和 query 231 | url += extname + query; 232 | 233 | return url; 234 | } 235 | /** 236 | * id normalize化 237 | * 238 | * @inner 239 | * @param {string} id 需要normalize的模块标识 240 | * @param {string} baseId 当前环境的模块标识 241 | * @return {string} 242 | */ 243 | function normalize( id, baseId ) { 244 | if ( !id ) { 245 | return ''; 246 | } 247 | 248 | baseId = baseId || ''; 249 | var idInfo = parseId( id ); 250 | if ( !idInfo ) { 251 | return id; 252 | } 253 | 254 | var resourceId = idInfo.resource; 255 | var moduleId = relative2absolute( idInfo.module, baseId ); 256 | 257 | each( 258 | packagesIndex, 259 | function ( packageConf ) { 260 | var name = packageConf.name; 261 | if ( name === moduleId ) { 262 | moduleId = name + '/' + packageConf.main; 263 | return false; 264 | } 265 | } 266 | ); 267 | 268 | // 根据config中的map配置进行module id mapping 269 | indexRetrieve( 270 | baseId, 271 | mappingIdIndex, 272 | function ( value ) { 273 | 274 | indexRetrieve( 275 | moduleId, 276 | value, 277 | function ( mdValue, mdKey ) { 278 | moduleId = moduleId.replace( mdKey, mdValue ); 279 | } 280 | ); 281 | 282 | } 283 | ); 284 | 285 | if ( resourceId ) { 286 | moduleId += '!' + resourceId; 287 | } 288 | 289 | return moduleId; 290 | } 291 | 292 | /** 293 | * 相对id转换成绝对id 294 | * 295 | * @inner 296 | * @param {string} id 要转换的id 297 | * @param {string} baseId 当前所在环境id 298 | * @return {string} 299 | */ 300 | function relative2absolute( id, baseId ) { 301 | if ( id.indexOf( '.' ) === 0 ) { 302 | var basePath = baseId.split( '/' ); 303 | var namePath = id.split( '/' ); 304 | var baseLen = basePath.length - 1; 305 | var nameLen = namePath.length; 306 | var cutBaseTerms = 0; 307 | var cutNameTerms = 0; 308 | 309 | pathLoop: for ( var i = 0; i < nameLen; i++ ) { 310 | var term = namePath[ i ]; 311 | switch ( term ) { 312 | case '..': 313 | if ( cutBaseTerms < baseLen ) { 314 | cutBaseTerms++; 315 | cutNameTerms++; 316 | } 317 | else { 318 | break pathLoop; 319 | } 320 | break; 321 | case '.': 322 | cutNameTerms++; 323 | break; 324 | default: 325 | break pathLoop; 326 | } 327 | } 328 | 329 | basePath.length = baseLen - cutBaseTerms; 330 | namePath = namePath.slice( cutNameTerms ); 331 | 332 | return basePath.concat( namePath ).join( '/' ); 333 | } 334 | 335 | return id; 336 | } 337 | 338 | /** 339 | * 解析id,返回带有module和resource属性的Object 340 | * 341 | * @inner 342 | * @param {string} id 标识 343 | * @return {Object} 344 | */ 345 | function parseId( id ) { 346 | var segs = id.split( '!' ); 347 | 348 | if ( /^[-_a-z0-9\.]+(\/[-_a-z0-9\.]+)*$/i.test( segs[ 0 ] ) ) { 349 | return { 350 | module : segs[ 0 ], 351 | resource : segs[ 1 ] 352 | }; 353 | } 354 | 355 | return null; 356 | } 357 | 358 | exports.parseId = parseId; 359 | exports.normalize = normalize; 360 | exports.toUrl = toUrl; 361 | exports.config = function ( conf ) { 362 | requireConf = conf; 363 | createConfIndex(); 364 | }; 365 | 366 | exports.getPackageInfo = function (id) { 367 | var info; 368 | 369 | indexRetrieve( 370 | id, 371 | packagesIndex, 372 | function ( value, key, item ) { 373 | info = item; 374 | } 375 | ); 376 | 377 | return info; 378 | }; 379 | -------------------------------------------------------------------------------- /lib/analyse.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var esprima = require('esprima'); 4 | var estraverse = require('estraverse'); 5 | 6 | var SYNTAX = estraverse.Syntax; 7 | var BUILTIN_MODULES = { 8 | 'require': 1, 9 | 'exports': 1, 10 | 'module': 1 11 | }; 12 | var analysedModules = {}; 13 | 14 | function analyse(id, deep, visited, exclude) { 15 | visited = visited || {}; 16 | exclude = exclude || []; 17 | 18 | if (visited[id]) { 19 | return false; 20 | } 21 | 22 | var idInfo = amd.parseId(id); 23 | if (!idInfo) { 24 | return false; 25 | } 26 | var moduleId = idInfo.module; 27 | var resourceId = idInfo.resource; 28 | 29 | var source = require('./read-module-file')(moduleId); 30 | if (!source) { 31 | return false; 32 | } 33 | 34 | var moduleInfo = analysedModules[id]; 35 | if (!moduleInfo) { 36 | 37 | if (resourceId) { 38 | analysedModules[id] = { 39 | id: moduleId, 40 | resourceId: resourceId, 41 | // Resources don't have any dependencies 42 | literalDependencies: [], 43 | dependencies: [], 44 | builtCode: genResourceModuleCode(moduleId, resourceId) 45 | }; 46 | 47 | return false; 48 | } 49 | 50 | var packageInfo = amd.getPackageInfo(moduleId); 51 | if (packageInfo && packageInfo.name === moduleId) { 52 | var realdep = moduleId + '/' + packageInfo.main; 53 | var realDepsIndex = {}; 54 | realDepsIndex[realdep] = 1; 55 | moduleInfo = { 56 | id: moduleId, 57 | dependencies: [realdep], 58 | literalDependencies: [realdep], 59 | dependenciesIndex: realDepsIndex, 60 | builtCode: '' 61 | }; 62 | 63 | analysedModules[id] = moduleInfo; 64 | } 65 | else { 66 | var ast = esprima.parse(source,{ 67 | // raw: true, 68 | // tokens: true, 69 | // range: true, 70 | // comment: true 71 | }); 72 | 73 | estraverse.traverse(ast, { 74 | enter: function (node) { 75 | if (node.type == SYNTAX.CallExpression 76 | && node.callee.name == 'define' 77 | ) { 78 | this.skip(); 79 | moduleInfo = analyseDefineNode(node, moduleId); 80 | analysedModules[id] = moduleInfo; 81 | } 82 | } 83 | }); 84 | 85 | moduleInfo.ast = ast; 86 | genBuiltCode(moduleInfo); 87 | } 88 | } 89 | 90 | var result = moduleInfo.dependencies.filter(function (dep) { 91 | return !isModuleExcluded(exclude, dep); 92 | }); 93 | 94 | if (deep) { 95 | var depDeps = []; 96 | result.forEach(function (dep) { 97 | if (BUILTIN_MODULES[dep]) { 98 | return; 99 | } 100 | 101 | var currentDepDeps = analyse(dep, 1, visited, exclude); 102 | if (currentDepDeps !== false) { 103 | depDeps.push.apply(depDeps, currentDepDeps); 104 | } 105 | }); 106 | 107 | var depDepsMap = {}; 108 | depDeps.forEach(function (depDep){ 109 | if (moduleInfo.dependenciesIndex[depDep]) { 110 | depDepsMap[depDep] = 1; 111 | } 112 | else if (!depDepsMap[depDep]) { 113 | result.push(depDep); 114 | depDepsMap[depDep] = 1; 115 | } 116 | }); 117 | } 118 | 119 | return result; 120 | } 121 | 122 | function genResourceModuleCode(moduleId, resourceId) { 123 | // Read resource file with extension 124 | var ext = require(amd.toUrl(moduleId)); 125 | var content = ext.write(moduleId, resourceId, require('./read-resource-file')(resourceId)); 126 | 127 | return [";\ndefine('", resourceId, "', function() { return '", content, , "'});\n"].join(''); 128 | } 129 | 130 | /** 131 | * 根据给定的 exclude 配置项判断模块是否不打包 132 | * @param {Array} exclude 133 | * @param {string} moduleId 134 | * @return {boolean} 135 | */ 136 | function isModuleExcluded(exclude, moduleId) { 137 | var packageInfo = amd.getPackageInfo(moduleId); 138 | return (exclude.indexOf(moduleId) >=0) 139 | || (packageInfo && exclude.indexOf(packageInfo.name) >= 0); 140 | } 141 | 142 | var amd = require('./amd'); 143 | 144 | /** 145 | * define分析 146 | * 147 | * @inner 148 | * @param {Object} node define的语法树节点 149 | */ 150 | function analyseDefineNode(node, moduleId) { 151 | var args = node.arguments; 152 | var argsLen = args.length; 153 | var factory = args[ --argsLen ]; 154 | var factoryArgLen = factory.type == SYNTAX.FunctionExpression 155 | ? factory.params.length 156 | : 0; 157 | 158 | var dependencies = ['require', 'exports', 'module'].slice(0, factoryArgLen); 159 | var id = moduleId; 160 | while ( argsLen-- ) { 161 | var arg = args[ argsLen ]; 162 | if ( arg.type == SYNTAX.ArrayExpression ) { 163 | dependencies = ast2obj( arg ); 164 | } 165 | else if ( arg.type == SYNTAX.Literal 166 | && typeof arg.value == 'string' 167 | ) { 168 | id = arg.value; 169 | } 170 | } 171 | 172 | if ( !id ) { 173 | return; 174 | } 175 | 176 | var functionLevel = -1; 177 | 178 | var literalDependenciesMap = {}; 179 | function addLiteralDependency( mId, rId ) { 180 | var key = mId; 181 | if (rId) { 182 | key += '!' + rId; 183 | } 184 | var depObj = literalDependenciesMap[ key ]; 185 | if ( !depObj ) { 186 | dependencies.push( key ); 187 | literalDependenciesMap[ key ] = 1; 188 | } 189 | } 190 | dependencies.forEach(function ( dep ) { 191 | literalDependenciesMap[dep] = 1; 192 | }); 193 | 194 | var realDependencies = []; 195 | var realDependenciesMap = {}; 196 | function addRealDependency( mId, rId ) { 197 | var key = mId; 198 | if (rId) { 199 | key += '!' + rId; 200 | } 201 | var depObj = realDependenciesMap[ key ]; 202 | if ( !depObj ) { 203 | realDependencies.push( key ); 204 | realDependenciesMap[ key ] = 1; 205 | } 206 | } 207 | 208 | dependencies.forEach( function ( dep, index ) { 209 | var idInfo = amd.parseId( dep ); 210 | var mId = idInfo.module; 211 | var rId = idInfo.resource; 212 | addRealDependency( 213 | amd.normalize(mId, id ), 214 | rId && amd.normalize(rId, id) 215 | ); 216 | } ); 217 | 218 | if ( factory.type == SYNTAX.FunctionExpression ) { 219 | 220 | var requireFormalParameter = findRequireFormalParameter({ 221 | factoryAst: factory, 222 | dependencies: dependencies 223 | }); 224 | 225 | estraverse.traverse( factory, { 226 | enter: function ( node ) { 227 | var requireArg0; 228 | 229 | switch ( node.type ) { 230 | case SYNTAX.FunctionExpression: 231 | case SYNTAX.FunctionDeclaration: 232 | functionLevel++; 233 | break; 234 | case SYNTAX.CallExpression: 235 | if ( requireFormalParameter 236 | && node.callee.name == requireFormalParameter 237 | && (requireArg0 = node.arguments[0]) 238 | && requireArg0.type == SYNTAX.Literal 239 | && typeof requireArg0.value == 'string' 240 | ) { 241 | var idInfo = amd.parseId(requireArg0.value); 242 | var mId = idInfo.module; 243 | var rId = idInfo.resource; 244 | addLiteralDependency( mId, rId ); 245 | addRealDependency( 246 | amd.normalize( mId, id ), 247 | rId && amd.normalize( rId, id ) 248 | ); 249 | } 250 | break; 251 | } 252 | }, 253 | 254 | leave: function ( node ) { 255 | switch ( node.type ) { 256 | case SYNTAX.FunctionExpression: 257 | case SYNTAX.FunctionDeclaration: 258 | functionLevel--; 259 | break; 260 | } 261 | } 262 | } ); 263 | } 264 | 265 | return { 266 | id: id, 267 | factoryAst: factory, 268 | literalDependencies: dependencies, 269 | dependencies: realDependencies, 270 | dependenciesIndex: realDependenciesMap 271 | }; 272 | } 273 | 274 | function findRequireFormalParameter(moduleInfo) { 275 | var requireFormalParameter; 276 | var factory = moduleInfo.factoryAst; 277 | var dependencies = moduleInfo.dependencies; 278 | dependencies.forEach( function ( dep, index ) { 279 | if ( index >= factory.params.length ) { 280 | return false; 281 | } 282 | 283 | if ( dep === 'require' ) { 284 | requireFormalParameter = factory.params[ index ].name; 285 | return false; 286 | } 287 | }); 288 | return requireFormalParameter; 289 | } 290 | 291 | function genBuiltCode(moduleInfo) { 292 | var ast = moduleInfo.ast; 293 | 294 | var requireFormalParameter = findRequireFormalParameter(moduleInfo); 295 | 296 | estraverse.replace( ast, { 297 | enter: function ( node ) { 298 | var requireArg0; 299 | if ( node.type === SYNTAX.CallExpression 300 | && node.callee.name === 'define' 301 | ) { 302 | return generateModuleAst(moduleInfo); 303 | } 304 | else if ( node.type === SYNTAX.CallExpression 305 | && requireFormalParameter 306 | && node.callee.name == requireFormalParameter 307 | && (requireArg0 = node.arguments[0]) 308 | && requireArg0.type == SYNTAX.Literal 309 | && typeof requireArg0.value == 'string' 310 | ) { 311 | var idInfo = amd.parseId(requireArg0.value); 312 | var mId = idInfo.module; 313 | var rId = idInfo.resource; 314 | // Replace the moduleId!resourceId with resourceId 315 | if (rId) { 316 | requireArg0.value = rId; 317 | } 318 | } 319 | } 320 | } ); 321 | 322 | var escodegen = require('escodegen'); 323 | moduleInfo.builtCode = escodegen.generate( 324 | ast, 325 | { 326 | format: {escapeless: true}, 327 | comment: true 328 | } 329 | ); 330 | 331 | var moduleId = moduleInfo.id; 332 | var packageInfo = amd.getPackageInfo(moduleId); 333 | if (moduleId === packageInfo.name + '/' + packageInfo.main) { 334 | moduleInfo.builtCode = 'define(\'' + packageInfo.name 335 | + '\', [\'' + moduleId 336 | + '\'], function (main) {return main;});\n' 337 | + moduleInfo.builtCode; 338 | } 339 | 340 | } 341 | 342 | function generateModuleAst( moduleInfo ) { 343 | var dependenciesExpr; 344 | var dependencies = moduleInfo.literalDependencies; 345 | if ( dependencies instanceof Array ) { 346 | dependenciesExpr = { 347 | type: SYNTAX.ArrayExpression, 348 | elements: [] 349 | }; 350 | 351 | dependencies.forEach( function ( dependency ) { 352 | var idInfo = amd.parseId(dependency); 353 | dependency = idInfo.resource || dependency; 354 | dependenciesExpr.elements.push( { 355 | type: SYNTAX.Literal, 356 | value: dependency, 357 | raw: '\'' + dependency + '\'' 358 | }); 359 | } ); 360 | } 361 | 362 | var defineArgs = [ moduleInfo.factoryAst ]; 363 | if ( dependenciesExpr ) { 364 | defineArgs.unshift( dependenciesExpr ); 365 | } 366 | var id = moduleInfo.id; 367 | if ( id ) { 368 | defineArgs.unshift( { 369 | type: SYNTAX.Literal, 370 | value: moduleInfo.id, 371 | raw: '\'' + moduleInfo.id + '\'' 372 | } ); 373 | } 374 | 375 | return { 376 | type: SYNTAX.CallExpression, 377 | callee: { 378 | type: SYNTAX.Identifier, 379 | name: 'define' 380 | }, 381 | 'arguments': defineArgs 382 | }; 383 | } 384 | 385 | /** 386 | * 根据抽象语法树获取对象的值,仅支持Array,Object,Literal(boolean,string,number) 387 | * 388 | * 由于chrome extension的沙箱限制,不能eval和new Function 389 | * 所以不能根据语法树生成source再eval,只好自己写了个这货 390 | * 391 | * @param {Object} ast 语法树节点 392 | * @return {*} 393 | */ 394 | function ast2obj( ast ) { 395 | 396 | /** 397 | * 解析对象 398 | * 399 | * @inner 400 | * @param {Object} node 语法树节点 401 | * @return {Object} 402 | */ 403 | function parseObject( node ) { 404 | var value = {}; 405 | node.properties.forEach( function ( prop ) { 406 | value[ prop.key.name || prop.key.value ] = parse( prop.value ); 407 | }); 408 | 409 | return value; 410 | } 411 | 412 | /** 413 | * 解析数组 414 | * 415 | * @inner 416 | * @param {Object} node 语法树节点 417 | * @return {Array} 418 | */ 419 | function parseArray( node ) { 420 | var value = []; 421 | node.elements.forEach( function ( element ) { 422 | value.push( parse( element ) ); 423 | }); 424 | 425 | return value; 426 | } 427 | 428 | /** 429 | * 解析Literal 430 | * 431 | * @inner 432 | * @param {Object} node 语法树节点 433 | * @return {*} 434 | */ 435 | function parseLiteral( node ) { 436 | return node.value; 437 | } 438 | 439 | /** 440 | * 解析节点 441 | * 442 | * @inner 443 | * @param {Object} node 语法树节点 444 | * @return {*} 445 | */ 446 | function parse( node ) { 447 | switch (node.type) { 448 | case SYNTAX.ObjectExpression: 449 | return parseObject( node ); 450 | case SYNTAX.Literal: 451 | return parseLiteral( node ); 452 | case SYNTAX.ArrayExpression: 453 | return parseArray( node ); 454 | default: 455 | throw new Error( '[RAWOBJECT_FAIL]' ); 456 | } 457 | } 458 | 459 | return parse( ast ); 460 | } 461 | 462 | exports = module.exports = analyse; 463 | 464 | 465 | exports.getAnalysed = function (id) { 466 | if (!analysedModules[id]) { 467 | analyse(id, 1); 468 | } 469 | 470 | return analysedModules[id]; 471 | }; 472 | -------------------------------------------------------------------------------- /lib/etpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ETPL (Enterprise Template) 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 模板引擎 6 | * @author errorrik(errorrik@gmail.com) 7 | * otakustay(otakustay@gmail.com) 8 | */ 9 | 10 | 11 | // HACK: 可见的重复代码未抽取成function和var是为了gzip size,吐槽的一边去 12 | 13 | (function (root) { 14 | /** 15 | * 对象属性拷贝 16 | * 17 | * @inner 18 | * @param {Object} target 目标对象 19 | * @param {Object} source 源对象 20 | * @return {Object} 返回目标对象 21 | */ 22 | function extend(target, source) { 23 | for (var key in source) { 24 | if (source.hasOwnProperty(key)) { 25 | target[key] = source[key]; 26 | } 27 | } 28 | 29 | return target; 30 | } 31 | 32 | /** 33 | * 随手写了个栈 34 | * 35 | * @inner 36 | * @constructor 37 | */ 38 | function Stack() { 39 | this.raw = []; 40 | this.length = 0; 41 | } 42 | 43 | Stack.prototype = { 44 | /** 45 | * 添加元素进栈 46 | * 47 | * @param {*} elem 添加项 48 | */ 49 | push: function (elem) { 50 | this.raw[this.length++] = elem; 51 | }, 52 | 53 | /** 54 | * 弹出顶部元素 55 | * 56 | * @return {*} 57 | */ 58 | pop: function () { 59 | if (this.length > 0) { 60 | var elem = this.raw[--this.length]; 61 | this.raw.length = this.length; 62 | return elem; 63 | } 64 | }, 65 | 66 | /** 67 | * 获取顶部元素 68 | * 69 | * @return {*} 70 | */ 71 | top: function () { 72 | return this.raw[this.length - 1]; 73 | }, 74 | 75 | /** 76 | * 获取底部元素 77 | * 78 | * @return {*} 79 | */ 80 | bottom: function () { 81 | return this.raw[0]; 82 | }, 83 | 84 | /** 85 | * 根据查询条件获取元素 86 | * 87 | * @param {Function} condition 查询函数 88 | * @return {*} 89 | */ 90 | find: function (condition) { 91 | var index = this.length; 92 | while (index--) { 93 | var item = this.raw[index]; 94 | if (condition(item)) { 95 | return item; 96 | } 97 | } 98 | } 99 | }; 100 | 101 | /** 102 | * 唯一id的起始值 103 | * 104 | * @inner 105 | * @type {number} 106 | */ 107 | var guidIndex = 0x2B845; 108 | 109 | /** 110 | * 获取唯一id,用于匿名target或编译代码的变量名生成 111 | * 112 | * @inner 113 | * @return {string} 114 | */ 115 | function generateGUID() { 116 | return '___' + (guidIndex++); 117 | } 118 | 119 | /** 120 | * 构建类之间的继承关系 121 | * 122 | * @inner 123 | * @param {Function} subClass 子类函数 124 | * @param {Function} superClass 父类函数 125 | */ 126 | function inherits(subClass, superClass) { 127 | /* jshint -W054 */ 128 | var F = new Function(); 129 | F.prototype = superClass.prototype; 130 | subClass.prototype = new F(); 131 | subClass.prototype.constructor = subClass; 132 | /* jshint +W054 */ 133 | // 由于引擎内部的使用场景都是inherits后,逐个编写子类的prototype方法 134 | // 所以,不考虑将原有子类prototype缓存再逐个拷贝回去 135 | } 136 | 137 | /** 138 | * HTML Filter替换的字符实体表 139 | * 140 | * @const 141 | * @inner 142 | * @type {Object} 143 | */ 144 | var HTML_ENTITY = { 145 | /* jshint ignore:start */ 146 | '&': '&', 147 | '<': '<', 148 | '>': '>', 149 | '"': '"', 150 | "'": ''' 151 | /* jshint ignore:end */ 152 | }; 153 | 154 | /** 155 | * HTML Filter的替换函数 156 | * 157 | * @inner 158 | * @param {string} c 替换字符 159 | * @return {string} 160 | */ 161 | function htmlFilterReplacer(c) { 162 | return HTML_ENTITY[c]; 163 | } 164 | 165 | /** 166 | * 默认filter 167 | * 168 | * @inner 169 | * @const 170 | * @type {Object} 171 | */ 172 | var DEFAULT_FILTERS = { 173 | /** 174 | * HTML转义filter 175 | * 176 | * @param {string} source 源串 177 | * @return {string} 178 | */ 179 | html: function (source) { 180 | return source.replace(/[&<>"']/g, htmlFilterReplacer); 181 | }, 182 | 183 | /** 184 | * URL编码filter 185 | * 186 | * @param {string} source 源串 187 | * @return {string} 188 | */ 189 | url: encodeURIComponent, 190 | 191 | /** 192 | * 源串filter,用于在默认开启HTML转义时获取源串,不进行转义 193 | * 194 | * @param {string} source 源串 195 | * @return {string} 196 | */ 197 | raw: function (source) { 198 | return source; 199 | } 200 | }; 201 | 202 | /** 203 | * 字符串字面化 204 | * 205 | * @inner 206 | * @param {string} source 需要字面化的字符串 207 | * @return {string} 208 | */ 209 | function stringLiteralize(source) { 210 | return '"' 211 | + source 212 | .replace(/\x5C/g, '\\\\') 213 | .replace(/"/g, '\\"') 214 | .replace(/\x0A/g, '\\n') 215 | .replace(/\x09/g, '\\t') 216 | .replace(/\x0D/g, '\\r') 217 | // .replace( /\x08/g, '\\b' ) 218 | // .replace( /\x0C/g, '\\f' ) 219 | + '"'; 220 | } 221 | 222 | /** 223 | * 对字符串进行可用于new RegExp的字面化 224 | * 225 | * @inner 226 | * @param {string} source 需要字面化的字符串 227 | * @return {string} 228 | */ 229 | function regexpLiteral(source) { 230 | return source.replace(/[\^\[\]\$\(\)\{\}\?\*\.\+]/g, function (c) { 231 | return '\\' + c; 232 | }); 233 | } 234 | 235 | /** 236 | * 字符串格式化 237 | * 238 | * @inner 239 | * @param {string} source 目标模版字符串 240 | * @param {...string} replacements 字符串替换项集合 241 | * @return {string} 242 | */ 243 | function stringFormat(source) { 244 | var args = arguments; 245 | return source.replace( 246 | /\{([0-9]+)\}/g, 247 | function (match, index) { 248 | return args[index - 0 + 1]; 249 | }); 250 | } 251 | 252 | /** 253 | * 用于render的字符串变量声明语句 254 | * 255 | * @inner 256 | * @const 257 | * @type {string} 258 | */ 259 | var RENDER_STRING_DECLATION = 'var r="";'; 260 | 261 | /** 262 | * 用于render的字符串内容添加语句(起始) 263 | * 264 | * @inner 265 | * @const 266 | * @type {string} 267 | */ 268 | var RENDER_STRING_ADD_START = 'r+='; 269 | 270 | /** 271 | * 用于render的字符串内容添加语句(结束) 272 | * 273 | * @inner 274 | * @const 275 | * @type {string} 276 | */ 277 | var RENDER_STRING_ADD_END = ';'; 278 | 279 | /** 280 | * 用于render的字符串内容返回语句 281 | * 282 | * @inner 283 | * @const 284 | * @type {string} 285 | */ 286 | var RENDER_STRING_RETURN = 'return r;'; 287 | 288 | // HACK: IE8-时,编译后的renderer使用join Array的策略进行字符串拼接 289 | if (typeof navigator !== 'undefined' 290 | && /msie\s*([0-9]+)/i.test(navigator.userAgent) 291 | && RegExp.$1 - 0 < 8 292 | ) { 293 | RENDER_STRING_DECLATION = 'var r=[],ri=0;'; 294 | RENDER_STRING_ADD_START = 'r[ri++]='; 295 | RENDER_STRING_RETURN = 'return r.join("");'; 296 | } 297 | 298 | /** 299 | * 将访问变量名称转换成getVariable调用的编译语句 300 | * 用于if、var等命令生成编译代码 301 | * 302 | * @inner 303 | * @param {string} name 访问变量名 304 | * @return {string} 305 | */ 306 | function toGetVariableLiteral(name) { 307 | name = name.replace(/^\s*\*/, ''); 308 | return stringFormat( 309 | 'gv({0},["{1}"])', 310 | stringLiteralize(name), 311 | name.replace( 312 | /\[['"]?([^'"]+)['"]?\]/g, 313 | function (match, name) { 314 | return '.' + name; 315 | } 316 | ) 317 | .split('.') 318 | .join('","') 319 | ); 320 | } 321 | 322 | /** 323 | * 解析文本片段中以固定字符串开头和结尾的包含块 324 | * 用于 命令串: 和 变量替换串:${...} 的解析 325 | * 326 | * @inner 327 | * @param {string} source 要解析的文本 328 | * @param {string} open 包含块开头 329 | * @param {string} close 包含块结束 330 | * @param {boolean} greedy 是否贪婪匹配 331 | * @param {function({string})} onInBlock 包含块内文本的处理函数 332 | * @param {function({string})} onOutBlock 非包含块内文本的处理函数 333 | */ 334 | function parseTextBlock(source, open, close, greedy, onInBlock, onOutBlock) { 335 | var closeLen = close.length; 336 | var texts = source.split(open); 337 | var level = 0; 338 | var buf = []; 339 | 340 | for (var i = 0, len = texts.length; i < len; i++) { 341 | var text = texts[i]; 342 | 343 | if (i) { 344 | var openBegin = 1; 345 | level++; 346 | while (1) { 347 | var closeIndex = text.indexOf(close); 348 | if (closeIndex < 0) { 349 | buf.push(level > 1 && openBegin ? open : '', text); 350 | break; 351 | } 352 | 353 | level = greedy ? level - 1 : 0; 354 | buf.push( 355 | level > 0 && openBegin ? open : '', 356 | text.slice(0, closeIndex), 357 | level > 0 ? close : '' 358 | ); 359 | text = text.slice(closeIndex + closeLen); 360 | openBegin = 0; 361 | 362 | if (level === 0) { 363 | break; 364 | } 365 | } 366 | 367 | if (level === 0) { 368 | onInBlock(buf.join('')); 369 | onOutBlock(text); 370 | buf = []; 371 | } 372 | } 373 | else { 374 | text && onOutBlock(text); 375 | } 376 | } 377 | 378 | if (level > 0 && buf.length > 0) { 379 | onOutBlock(open); 380 | onOutBlock(buf.join('')); 381 | } 382 | } 383 | 384 | /** 385 | * 编译变量访问和变量替换的代码 386 | * 用于普通文本或if、var、filter等命令生成编译代码 387 | * 388 | * @inner 389 | * @param {string} source 源代码 390 | * @param {Engine} engine 引擎实例 391 | * @param {boolean} forText 是否为输出文本的变量替换 392 | * @return {string} 393 | */ 394 | function compileVariable(source, engine, forText) { 395 | var code = []; 396 | var options = engine.options; 397 | 398 | var toStringHead = ''; 399 | var toStringFoot = ''; 400 | var wrapHead = ''; 401 | var wrapFoot = ''; 402 | 403 | // 默认的filter,当forText模式时有效 404 | var defaultFilter; 405 | 406 | if (forText) { 407 | toStringHead = 'ts('; 408 | toStringFoot = ')'; 409 | wrapHead = RENDER_STRING_ADD_START; 410 | wrapFoot = RENDER_STRING_ADD_END; 411 | defaultFilter = options.defaultFilter; 412 | } 413 | 414 | parseTextBlock( 415 | source, options.variableOpen, options.variableClose, 1, 416 | 417 | function (text) { 418 | // 加入默认filter 419 | // 只有当处理forText时,需要加入默认filter 420 | // 处理if/var/use等command时,不需要加入默认filter 421 | if (forText && text.indexOf('|') < 0 && defaultFilter) { 422 | text += '|' + defaultFilter; 423 | } 424 | 425 | // variableCode是一个gv调用,然后通过循环,在外面包filter的调用 426 | // 形成filter["b"](filter["a"](gv(...))) 427 | // 428 | // 当forText模式,处理的是文本中的变量替换时 429 | // 传递给filter的需要是字符串形式,所以gv外需要包一层ts调用 430 | // 形成filter["b"](filter["a"](ts(gv(...)))) 431 | // 432 | // 当variableName以*起始时,忽略ts调用,直接传递原值给filter 433 | var filterCharIndex = text.indexOf('|'); 434 | var variableName = ( 435 | filterCharIndex > 0 436 | ? text.slice(0, filterCharIndex) 437 | : text 438 | ).replace(/^\s+/, '').replace(/\s+$/, ''); 439 | var filterSource = filterCharIndex > 0 440 | ? text.slice(filterCharIndex + 1) 441 | : ''; 442 | 443 | var variableRawValue = variableName.indexOf('*') === 0; 444 | var variableCode = [ 445 | variableRawValue ? '' : toStringHead, 446 | toGetVariableLiteral(variableName), 447 | variableRawValue ? '' : toStringFoot 448 | ]; 449 | 450 | if (filterSource) { 451 | filterSource = compileVariable(filterSource, engine); 452 | var filterSegs = filterSource.split('|'); 453 | for (var i = 0, len = filterSegs.length; i < len; i++) { 454 | var seg = filterSegs[i]; 455 | 456 | if (/^\s*([a-z0-9_-]+)(\((.*)\))?\s*$/i.test(seg)) { 457 | variableCode.unshift('fs["' + RegExp.$1 + '"]('); 458 | 459 | if (RegExp.$3) { 460 | variableCode.push(',', RegExp.$3); 461 | } 462 | 463 | variableCode.push(')'); 464 | } 465 | } 466 | } 467 | 468 | code.push( 469 | wrapHead, 470 | variableCode.join(''), 471 | wrapFoot 472 | ); 473 | }, 474 | 475 | function (text) { 476 | code.push( 477 | wrapHead, 478 | forText ? stringLiteralize(text) : text, 479 | wrapFoot 480 | ); 481 | } 482 | ); 483 | 484 | return code.join(''); 485 | } 486 | 487 | /** 488 | * 文本节点类 489 | * 490 | * @inner 491 | * @constructor 492 | * @param {string} value 文本节点的内容文本 493 | * @param {Engine} engine 引擎实例 494 | */ 495 | function TextNode(value, engine) { 496 | this.value = value; 497 | this.engine = engine; 498 | } 499 | 500 | TextNode.prototype = { 501 | /** 502 | * 获取renderer body的生成代码 503 | * 504 | * @return {string} 505 | */ 506 | getRendererBody: function () { 507 | var value = this.value; 508 | var options = this.engine.options; 509 | 510 | if (!value 511 | || (options.strip && /^\s*$/.test(value)) 512 | ) { 513 | return ''; 514 | } 515 | 516 | return compileVariable(value, this.engine, 1); 517 | }, 518 | 519 | /** 520 | * 复制节点的方法 521 | * 522 | * @return {TextNode} 523 | */ 524 | clone: function () { 525 | return this; 526 | } 527 | }; 528 | 529 | /** 530 | * 命令节点类 531 | * 532 | * @inner 533 | * @constructor 534 | * @param {string} value 命令节点的value 535 | * @param {Engine} engine 引擎实例 536 | */ 537 | function Command(value, engine) { 538 | this.value = value; 539 | this.engine = engine; 540 | this.children = []; 541 | this.cloneProps = []; 542 | } 543 | 544 | Command.prototype = { 545 | /** 546 | * 添加子节点 547 | * 548 | * @param {TextNode|Command} node 子节点 549 | */ 550 | addChild: function (node) { 551 | this.children.push(node); 552 | }, 553 | 554 | /** 555 | * 节点open,解析开始 556 | * 557 | * @param {Object} context 语法分析环境对象 558 | */ 559 | open: function (context) { 560 | var parent = context.stack.top(); 561 | parent && parent.addChild(this); 562 | context.stack.push(this); 563 | }, 564 | 565 | /** 566 | * 节点闭合,解析结束 567 | * 568 | * @param {Object} context 语法分析环境对象 569 | */ 570 | close: function (context) { 571 | if (context.stack.top() === this) { 572 | context.stack.pop(); 573 | } 574 | }, 575 | 576 | /** 577 | * 获取renderer body的生成代码 578 | * 579 | * @return {string} 580 | */ 581 | getRendererBody: function () { 582 | var buf = []; 583 | var children = this.children; 584 | for (var i = 0; i < children.length; i++) { 585 | buf.push(children[i].getRendererBody()); 586 | } 587 | 588 | return buf.join(''); 589 | }, 590 | 591 | /** 592 | * 复制节点的方法 593 | * 594 | * @return {Command} 595 | */ 596 | clone: function () { 597 | var node = new this.constructor(this.value, this.engine); 598 | for (var i = 0, l = this.children.length; i < l; i++) { 599 | node.addChild(this.children[i].clone()); 600 | } 601 | 602 | for (var i = 0, l = this.cloneProps.length; i < l; i++) { 603 | var prop = this.cloneProps[i]; 604 | node[prop] = this[prop]; 605 | } 606 | 607 | return node; 608 | } 609 | }; 610 | 611 | /** 612 | * 命令自动闭合 613 | * 614 | * @inner 615 | * @param {Object} context 语法分析环境对象 616 | * @param {Function=} CommandType 自闭合的节点类型 617 | */ 618 | function autoCloseCommand(context, CommandType) { 619 | var stack = context.stack; 620 | var closeEnd = CommandType 621 | ? stack.find( 622 | function (item) { 623 | return item instanceof CommandType; 624 | } 625 | ) 626 | : stack.bottom(); 627 | 628 | if (closeEnd) { 629 | var node; 630 | 631 | while ((node = stack.top()) !== closeEnd) { 632 | /* jshint ignore:start */ 633 | // 如果节点对象不包含autoClose方法 634 | // 则认为该节点不支持自动闭合,需要抛出错误 635 | // for等节点不支持自动闭合 636 | if (!node.autoClose) { 637 | throw new Error(node.type + ' must be closed manually: ' + node.value); 638 | } 639 | /* jshint ignore:end */ 640 | 641 | node.autoClose(context); 642 | } 643 | 644 | closeEnd.close(context); 645 | } 646 | 647 | return closeEnd; 648 | } 649 | 650 | /** 651 | * renderer body起始代码段 652 | * 653 | * @inner 654 | * @const 655 | * @type {string} 656 | */ 657 | var RENDERER_BODY_START = '' 658 | + 'data=data||{};' 659 | + 'var v={},fs=engine.filters,hg=typeof data.get=="function",' 660 | + 'gv=function(n,ps){' 661 | + 'var p=ps[0],d=v[p];' 662 | + 'if(d==null){' 663 | + 'if(hg){return data.get(n);}' 664 | + 'd=data[p];' 665 | + '}' 666 | + 'for(var i=1,l=ps.length;i= TargetState.APPLIED) { 923 | return 1; 924 | } 925 | 926 | var blocks = this.blocks; 927 | 928 | function replaceBlock(node) { 929 | var children = node.children; 930 | 931 | if (children instanceof Array) { 932 | for (var i = 0, len = children.length; i < len; i++) { 933 | var child = children[i]; 934 | if (child instanceof BlockCommand && blocks[child.name]) { 935 | child = children[i] = blocks[child.name]; 936 | } 937 | 938 | replaceBlock(child); 939 | } 940 | } 941 | } 942 | 943 | var master = this.engine.targets[masterName]; 944 | if (master && master.applyMaster(master.master)) { 945 | this.children = master.clone().children; 946 | replaceBlock(this); 947 | this.state = TargetState.APPLIED; 948 | return 1; 949 | } 950 | }; 951 | 952 | /** 953 | * 判断target是否ready 954 | * 包括是否成功应用母版,以及import语句依赖的target是否ready 955 | * 956 | * @return {boolean} 957 | */ 958 | TargetCommand.prototype.isReady = function () { 959 | if (this.state >= TargetState.READY) { 960 | return 1; 961 | } 962 | 963 | var engine = this.engine; 964 | var readyState = 1; 965 | 966 | /** 967 | * 递归检查节点的ready状态 968 | * 969 | * @inner 970 | * @param {Command|TextNode} node 目标节点 971 | */ 972 | function checkReadyState(node) { 973 | for (var i = 0, len = node.children.length; i < len; i++) { 974 | var child = node.children[i]; 975 | if (child instanceof ImportCommand) { 976 | var target = engine.targets[child.name]; 977 | readyState = readyState 978 | && target && target.isReady(engine); 979 | } 980 | else if (child instanceof Command) { 981 | checkReadyState(child); 982 | } 983 | } 984 | } 985 | 986 | if (this.applyMaster(this.master)) { 987 | checkReadyState(this); 988 | readyState && (this.state = TargetState.READY); 989 | return readyState; 990 | } 991 | }; 992 | 993 | /** 994 | * 获取target的renderer函数 995 | * 996 | * @return {function(Object):string} 997 | */ 998 | TargetCommand.prototype.getRenderer = function () { 999 | if (this.renderer) { 1000 | return this.renderer; 1001 | } 1002 | 1003 | if (this.isReady()) { 1004 | // console.log(this.name + ' ------------------'); 1005 | // console.log(RENDERER_BODY_START + RENDER_STRING_DECLATION 1006 | // + this.getRendererBody() 1007 | // + RENDER_STRING_RETURN); 1008 | 1009 | /* jshint -W054 */ 1010 | var realRenderer = new Function( 1011 | 'data', 'engine', 1012 | [ 1013 | RENDERER_BODY_START, 1014 | RENDER_STRING_DECLATION, 1015 | this.getRendererBody(), 1016 | RENDER_STRING_RETURN 1017 | ].join('\n') 1018 | ); 1019 | /* jshint +W054 */ 1020 | 1021 | var engine = this.engine; 1022 | this.renderer = function (data) { 1023 | return realRenderer(data, engine); 1024 | }; 1025 | 1026 | return this.renderer; 1027 | } 1028 | 1029 | return null; 1030 | }; 1031 | 1032 | /** 1033 | * 将target节点对象添加到语法分析环境中 1034 | * 1035 | * @inner 1036 | * @param {TargetCommand} target target节点对象 1037 | * @param {Object} context 语法分析环境对象 1038 | */ 1039 | function addTargetToContext(target, context) { 1040 | context.target = target; 1041 | 1042 | var engine = context.engine; 1043 | var name = target.name; 1044 | 1045 | if (engine.targets[name]) { 1046 | switch (engine.options.namingConflict) { 1047 | /* jshint ignore:start */ 1048 | case 'override': 1049 | engine.targets[name] = target; 1050 | context.targets.push(name); 1051 | case 'ignore': 1052 | break; 1053 | /* jshint ignore:end */ 1054 | default: 1055 | throw new Error('Target exists: ' + name); 1056 | } 1057 | } 1058 | else { 1059 | engine.targets[name] = target; 1060 | context.targets.push(name); 1061 | } 1062 | } 1063 | 1064 | /** 1065 | * target节点open,解析开始 1066 | * 1067 | * @param {Object} context 语法分析环境对象 1068 | */ 1069 | TargetCommand.prototype.open = function (context) { 1070 | autoCloseCommand(context); 1071 | Command.prototype.open.call(this, context); 1072 | this.state = TargetState.READING; 1073 | addTargetToContext(this, context); 1074 | }; 1075 | 1076 | /** 1077 | * Var节点open,解析开始 1078 | * 1079 | * @param {Object} context 语法分析环境对象 1080 | */ 1081 | VarCommand.prototype.open = 1082 | 1083 | /** 1084 | * Use节点open,解析开始 1085 | * 1086 | * @param {Object} context 语法分析环境对象 1087 | */ 1088 | UseCommand.prototype.open = function (context) { 1089 | context.stack.top().addChild(this); 1090 | }; 1091 | 1092 | /** 1093 | * Block节点open,解析开始 1094 | * 1095 | * @param {Object} context 语法分析环境对象 1096 | */ 1097 | BlockCommand.prototype.open = function (context) { 1098 | Command.prototype.open.call(this, context); 1099 | (context.imp || context.target).blocks[this.name] = this; 1100 | }; 1101 | 1102 | /** 1103 | * elif节点open,解析开始 1104 | * 1105 | * @param {Object} context 语法分析环境对象 1106 | */ 1107 | ElifCommand.prototype.open = function (context) { 1108 | var elseCommand = new ElseCommand(); 1109 | elseCommand.open(context); 1110 | 1111 | var ifCommand = autoCloseCommand(context, IfCommand); 1112 | ifCommand.addChild(this); 1113 | context.stack.push(this); 1114 | }; 1115 | 1116 | /** 1117 | * else节点open,解析开始 1118 | * 1119 | * @param {Object} context 语法分析环境对象 1120 | */ 1121 | ElseCommand.prototype.open = function (context) { 1122 | var ifCommand = autoCloseCommand(context, IfCommand); 1123 | ifCommand.addChild(this); 1124 | context.stack.push(this); 1125 | }; 1126 | 1127 | /** 1128 | * import节点open,解析开始 1129 | * 1130 | * @param {Object} context 语法分析环境对象 1131 | */ 1132 | ImportCommand.prototype.open = function (context) { 1133 | this.parent = context.stack.top(); 1134 | this.target = context.target; 1135 | Command.prototype.open.call(this, context); 1136 | this.state = TargetState.READING; 1137 | context.imp = this; 1138 | }; 1139 | 1140 | /** 1141 | * 节点解析结束 1142 | * 由于use节点无需闭合,处理时不会入栈,所以将close置为空函数 1143 | * 1144 | * @param {Object} context 语法分析环境对象 1145 | */ 1146 | UseCommand.prototype.close = 1147 | 1148 | /** 1149 | * 节点解析结束 1150 | * 由于var节点无需闭合,处理时不会入栈,所以将close置为空函数 1151 | * 1152 | * @param {Object} context 语法分析环境对象 1153 | */ 1154 | VarCommand.prototype.close = function () {}; 1155 | 1156 | /** 1157 | * 节点解析结束 1158 | * 1159 | * @param {Object} context 语法分析环境对象 1160 | */ 1161 | ImportCommand.prototype.close = function (context) { 1162 | Command.prototype.close.call(this, context); 1163 | this.state = TargetState.READED; 1164 | context.imp = null; 1165 | }; 1166 | 1167 | /** 1168 | * 节点闭合,解析结束 1169 | * 1170 | * @param {Object} context 语法分析环境对象 1171 | */ 1172 | TargetCommand.prototype.close = function (context) { 1173 | Command.prototype.close.call(this, context); 1174 | this.state = this.master ? TargetState.READED : TargetState.APPLIED; 1175 | context.target = null; 1176 | }; 1177 | 1178 | /** 1179 | * 节点自动闭合,解析结束 1180 | * ImportCommand的自动结束逻辑为,在其开始位置后马上结束 1181 | * 所以,其自动结束时children应赋予其所属的parent 1182 | * 1183 | * @param {Object} context 语法分析环境对象 1184 | */ 1185 | ImportCommand.prototype.autoClose = function (context) { 1186 | // move children to parent 1187 | var parentChildren = this.parent.children; 1188 | parentChildren.push.apply(parentChildren, this.children); 1189 | this.children.length = 0; 1190 | 1191 | // move blocks to target 1192 | for (var key in this.blocks) { 1193 | this.target.blocks[key] = this.blocks[key]; 1194 | } 1195 | this.blocks = {}; 1196 | 1197 | // do close 1198 | this.close(context); 1199 | }; 1200 | 1201 | /** 1202 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1203 | * 1204 | * @param {Object} context 语法分析环境对象 1205 | */ 1206 | UseCommand.prototype.beforeOpen = 1207 | 1208 | /** 1209 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1210 | * 1211 | * @param {Object} context 语法分析环境对象 1212 | */ 1213 | ImportCommand.prototype.beforeOpen = 1214 | 1215 | /** 1216 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1217 | * 1218 | * @param {Object} context 语法分析环境对象 1219 | */ 1220 | VarCommand.prototype.beforeOpen = 1221 | 1222 | /** 1223 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1224 | * 1225 | * @param {Object} context 语法分析环境对象 1226 | */ 1227 | ForCommand.prototype.beforeOpen = 1228 | 1229 | /** 1230 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1231 | * 1232 | * @param {Object} context 语法分析环境对象 1233 | */ 1234 | FilterCommand.prototype.beforeOpen = 1235 | 1236 | /** 1237 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1238 | * 1239 | * @param {Object} context 语法分析环境对象 1240 | */ 1241 | BlockCommand.prototype.beforeOpen = 1242 | 1243 | /** 1244 | * 节点open前的处理动作:节点不在target中时,自动创建匿名target 1245 | * 1246 | * @param {Object} context 语法分析环境对象 1247 | */ 1248 | IfCommand.prototype.beforeOpen = 1249 | 1250 | /** 1251 | * 文本节点被添加到分析环境前的处理动作:节点不在target中时,自动创建匿名target 1252 | * 1253 | * @param {Object} context 语法分析环境对象 1254 | */ 1255 | TextNode.prototype.beforeAdd = function (context) { 1256 | if (context.stack.bottom()) { 1257 | return; 1258 | } 1259 | 1260 | var target = new TargetCommand(generateGUID(), context.engine); 1261 | target.open(context); 1262 | }; 1263 | 1264 | /** 1265 | * 获取renderer body的生成代码 1266 | * 1267 | * @return {string} 1268 | */ 1269 | ImportCommand.prototype.getRendererBody = function () { 1270 | this.applyMaster(this.name); 1271 | return Command.prototype.getRendererBody.call(this); 1272 | }; 1273 | 1274 | /** 1275 | * 获取renderer body的生成代码 1276 | * 1277 | * @return {string} 1278 | */ 1279 | UseCommand.prototype.getRendererBody = function () { 1280 | return stringFormat( 1281 | '{0}engine.render({2},{{3}}){1}', 1282 | RENDER_STRING_ADD_START, 1283 | RENDER_STRING_ADD_END, 1284 | stringLiteralize(this.name), 1285 | compileVariable(this.args, this.engine).replace( 1286 | /(^|,)\s*([a-z0-9_]+)\s*=/ig, 1287 | function (match, start, argName) { 1288 | return (start || '') + stringLiteralize(argName) + ':'; 1289 | } 1290 | ) 1291 | ); 1292 | }; 1293 | 1294 | /** 1295 | * 获取renderer body的生成代码 1296 | * 1297 | * @return {string} 1298 | */ 1299 | VarCommand.prototype.getRendererBody = function () { 1300 | if (this.expr) { 1301 | return stringFormat( 1302 | 'v[{0}]={1};', 1303 | stringLiteralize(this.name), 1304 | compileVariable(this.expr, this.engine) 1305 | ); 1306 | } 1307 | 1308 | return ''; 1309 | }; 1310 | 1311 | /** 1312 | * 获取renderer body的生成代码 1313 | * 1314 | * @return {string} 1315 | */ 1316 | IfCommand.prototype.getRendererBody = function () { 1317 | return stringFormat( 1318 | 'if({0}){{1}}', 1319 | compileVariable(this.value, this.engine), 1320 | Command.prototype.getRendererBody.call(this) 1321 | ); 1322 | }; 1323 | 1324 | /** 1325 | * 获取renderer body的生成代码 1326 | * 1327 | * @return {string} 1328 | */ 1329 | ElseCommand.prototype.getRendererBody = function () { 1330 | return stringFormat( 1331 | '}else{{0}', 1332 | Command.prototype.getRendererBody.call(this) 1333 | ); 1334 | }; 1335 | 1336 | /** 1337 | * 获取renderer body的生成代码 1338 | * 1339 | * @return {string} 1340 | */ 1341 | ForCommand.prototype.getRendererBody = function () { 1342 | return stringFormat( 1343 | /* jshint ignore:start */ 1344 | '' 1345 | + 'var {0}={1};' 1346 | + 'if({0} instanceof Array)' 1347 | + 'for (var {4}=0,{5}={0}.length;{4}<{5};{4}++){v[{2}]={4};v[{3}]={0}[{4}];{6}}' 1348 | + 'else if(typeof {0}==="object")' 1349 | + 'for(var {4} in {0}){v[{2}]={4};v[{3}]={0}[{4}];{6}}', 1350 | /* jshint ignore:end */ 1351 | generateGUID(), 1352 | compileVariable(this.list, this.engine), 1353 | stringLiteralize(this.index || generateGUID()), 1354 | stringLiteralize(this.item), 1355 | generateGUID(), 1356 | generateGUID(), 1357 | Command.prototype.getRendererBody.call(this) 1358 | ); 1359 | }; 1360 | 1361 | /** 1362 | * 获取renderer body的生成代码 1363 | * 1364 | * @return {string} 1365 | */ 1366 | FilterCommand.prototype.getRendererBody = function () { 1367 | var args = this.args; 1368 | return stringFormat( 1369 | '{2}fs[{5}]((function(){{0}{4}{1}})(){6}){3}', 1370 | RENDER_STRING_DECLATION, 1371 | RENDER_STRING_RETURN, 1372 | RENDER_STRING_ADD_START, 1373 | RENDER_STRING_ADD_END, 1374 | Command.prototype.getRendererBody.call(this), 1375 | stringLiteralize(this.name), 1376 | args ? ',' + compileVariable(args, this.engine) : '' 1377 | ); 1378 | }; 1379 | 1380 | /** 1381 | * 命令类型集合 1382 | * 1383 | * @type {Object} 1384 | */ 1385 | var commandTypes = {}; 1386 | 1387 | /** 1388 | * 添加命令类型 1389 | * 1390 | * @inner 1391 | * @param {string} name 命令名称 1392 | * @param {Function} Type 处理命令用到的类 1393 | */ 1394 | function addCommandType(name, Type) { 1395 | commandTypes[name] = Type; 1396 | Type.prototype.type = name; 1397 | } 1398 | 1399 | addCommandType('target', TargetCommand); 1400 | addCommandType('block', BlockCommand); 1401 | addCommandType('import', ImportCommand); 1402 | addCommandType('use', UseCommand); 1403 | addCommandType('var', VarCommand); 1404 | addCommandType('for', ForCommand); 1405 | addCommandType('if', IfCommand); 1406 | addCommandType('elif', ElifCommand); 1407 | addCommandType('else', ElseCommand); 1408 | addCommandType('filter', FilterCommand); 1409 | 1410 | 1411 | /** 1412 | * etpl引擎类 1413 | * 1414 | * @constructor 1415 | * @param {Object=} options 引擎参数 1416 | * @param {string=} options.commandOpen 命令语法起始串 1417 | * @param {string=} options.commandClose 命令语法结束串 1418 | * @param {string=} options.variableOpen 变量语法起始串 1419 | * @param {string=} options.variableClose 变量语法结束串 1420 | * @param {string=} options.defaultFilter 默认变量替换的filter 1421 | * @param {boolean=} options.strip 是否清除命令标签前后的空白字符 1422 | * @param {string=} options.namingConflict target名字冲突时的处理策略 1423 | */ 1424 | function Engine(options) { 1425 | this.options = { 1426 | commandOpen: '', 1428 | commandSyntax: /^\s*(\/)?([a-z]+)\s*(?::([\s\S]*))?$/, 1429 | variableOpen: '${', 1430 | variableClose: '}', 1431 | defaultFilter: 'html' 1432 | }; 1433 | 1434 | this.config(options); 1435 | this.targets = {}; 1436 | this.filters = extend({}, DEFAULT_FILTERS); 1437 | } 1438 | 1439 | /** 1440 | * 配置引擎参数,设置的参数将被合并到现有参数中 1441 | * 1442 | * @param {Object} options 参数对象 1443 | * @param {string=} options.commandOpen 命令语法起始串 1444 | * @param {string=} options.commandClose 命令语法结束串 1445 | * @param {string=} options.variableOpen 变量语法起始串 1446 | * @param {string=} options.variableClose 变量语法结束串 1447 | * @param {string=} options.defaultFilter 默认变量替换的filter 1448 | * @param {boolean=} options.strip 是否清除命令标签前后的空白字符 1449 | * @param {string=} options.namingConflict target名字冲突时的处理策略 1450 | */ 1451 | Engine.prototype.config = function (options) { 1452 | extend(this.options, options); 1453 | }; 1454 | 1455 | /** 1456 | * 解析模板并编译,返回第一个target编译后的renderer函数。 1457 | * 1458 | * @param {string} source 模板源代码 1459 | * @return {function(Object):string} 1460 | */ 1461 | Engine.prototype.compile = 1462 | 1463 | /** 1464 | * 解析模板并编译,返回第一个target编译后的renderer函数。 1465 | * 该方法的存在为了兼容老模板引擎 1466 | * 1467 | * @param {string} source 模板源代码 1468 | * @return {function(Object):string} 1469 | */ 1470 | Engine.prototype.parse = function (source) { 1471 | if (source) { 1472 | var targetNames = parseSource(source, this); 1473 | if (targetNames.length) { 1474 | return this.targets[targetNames[0]].getRenderer(); 1475 | } 1476 | } 1477 | 1478 | /* jshint -W054 */ 1479 | return new Function('return ""'); 1480 | /* jshint +W054 */ 1481 | }; 1482 | 1483 | /** 1484 | * 根据target名称获取编译后的renderer函数 1485 | * 1486 | * @param {string} name target名称 1487 | * @return {function(Object):string} 1488 | */ 1489 | Engine.prototype.getRenderer = function (name) { 1490 | var target = this.targets[name]; 1491 | if (target) { 1492 | return target.getRenderer(); 1493 | } 1494 | }; 1495 | 1496 | /** 1497 | * 执行模板渲染,返回渲染后的字符串。 1498 | * 1499 | * @param {string} name target名称 1500 | * @param {Object=} data 模板数据。 1501 | * 可以是plain object, 1502 | * 也可以是带有 {string}get({string}name) 方法的对象 1503 | * @return {string} 1504 | */ 1505 | Engine.prototype.render = function (name, data) { 1506 | var renderer = this.getRenderer(name); 1507 | if (renderer) { 1508 | return renderer(data); 1509 | } 1510 | 1511 | return ''; 1512 | }; 1513 | 1514 | /** 1515 | * 增加过滤器 1516 | * 1517 | * @param {string} name 过滤器名称 1518 | * @param {Function} filter 过滤函数 1519 | */ 1520 | Engine.prototype.addFilter = function (name, filter) { 1521 | if (typeof filter === 'function') { 1522 | this.filters[name] = filter; 1523 | } 1524 | }; 1525 | 1526 | /** 1527 | * 解析源代码 1528 | * 1529 | * @inner 1530 | * @param {string} source 模板源代码 1531 | * @param {Engine} engine 引擎实例 1532 | * @return {Array} target名称列表 1533 | */ 1534 | function parseSource(source, engine) { 1535 | var commandOpen = engine.options.commandOpen; 1536 | var commandClose = engine.options.commandClose; 1537 | var commandSyntax = engine.options.commandSyntax; 1538 | 1539 | var stack = new Stack(); 1540 | var analyseContext = { 1541 | engine: engine, 1542 | targets: [], 1543 | stack: stack, 1544 | target: null 1545 | }; 1546 | 1547 | // text节点内容缓冲区,用于合并多text 1548 | var textBuf = []; 1549 | 1550 | /** 1551 | * 将缓冲区中的text节点内容写入 1552 | * 1553 | * @inner 1554 | */ 1555 | function flushTextBuf() { 1556 | var text; 1557 | if (textBuf.length > 0 && (text = textBuf.join(''))) { 1558 | var textNode = new TextNode(text, engine); 1559 | textNode.beforeAdd(analyseContext); 1560 | 1561 | stack.top().addChild(textNode); 1562 | textBuf = []; 1563 | 1564 | if (engine.options.strip 1565 | && analyseContext.current instanceof Command 1566 | ) { 1567 | textNode.value = text.replace(/^[\x20\t\r]*\n/, ''); 1568 | } 1569 | analyseContext.current = textNode; 1570 | } 1571 | } 1572 | 1573 | var NodeType; 1574 | 1575 | parseTextBlock( 1576 | source, commandOpen, commandClose, 0, 1577 | 1578 | function (text) { // 内文本的处理函数 1579 | var match = commandSyntax.exec(text); 1580 | 1581 | // 符合command规则,并且存在相应的Command类,说明是合法有含义的Command 1582 | // 否则,为不具有command含义的普通文本 1583 | if (match 1584 | && (NodeType = commandTypes[match[2].toLowerCase()]) 1585 | && typeof NodeType === 'function' 1586 | ) { 1587 | // 先将缓冲区中的text节点内容写入 1588 | flushTextBuf(); 1589 | 1590 | var currentNode = analyseContext.current; 1591 | if (engine.options.strip && currentNode instanceof TextNode) { 1592 | currentNode.value = currentNode.value 1593 | .replace(/\r?\n[\x20\t]*$/, '\n'); 1594 | } 1595 | 1596 | if (match[1]) { 1597 | currentNode = autoCloseCommand(analyseContext, NodeType); 1598 | } 1599 | else { 1600 | currentNode = new NodeType(match[3], engine); 1601 | if (typeof currentNode.beforeOpen === 'function') { 1602 | currentNode.beforeOpen(analyseContext); 1603 | } 1604 | currentNode.open(analyseContext); 1605 | } 1606 | 1607 | analyseContext.current = currentNode; 1608 | } 1609 | else if (!/^\s*\/\//.test(text)) { 1610 | // 如果不是模板注释,则作为普通文本,写入缓冲区 1611 | textBuf.push(commandOpen, text, commandClose); 1612 | } 1613 | 1614 | NodeType = null; 1615 | }, 1616 | 1617 | function (text) { // 外,普通文本的处理函数 1618 | // 普通文本直接写入缓冲区 1619 | textBuf.push(text); 1620 | } 1621 | ); 1622 | 1623 | 1624 | flushTextBuf(); // 将缓冲区中的text节点内容写入 1625 | autoCloseCommand(analyseContext); 1626 | 1627 | return analyseContext.targets; 1628 | } 1629 | 1630 | var etpl = new Engine(); 1631 | etpl.Engine = Engine; 1632 | 1633 | if (typeof exports === 'object' && typeof module === 'object') { 1634 | // For CommonJS 1635 | exports = module.exports = etpl; 1636 | } 1637 | else if (typeof define === 'function' && define.amd) { 1638 | // For AMD 1639 | define(etpl); 1640 | } 1641 | else { 1642 | // For