template-compiler
Version:
Template Compiler Tools
682 lines (488 loc) • 18 kB
JavaScript
/*!
* atc - Template Compiler
* https://github.com/cdc-im/atc
* Released under the MIT, BSD, and GPL Licenses
*/
'use strict';
var template = require('./lib/template.js');
var js_beautify = require('./lib/beautify.js');
var path = require('path');
var fs = require('fs');
var compiler = {
version: 'v1.0.2',
options: {
path: null,
output: null,
charset: 'utf-8',
watch: false,
cloneHelpers: false,
defineSyntax: false
},
/** 显示帮助 */
help: function () {
this.log('Usage:\n');
console.log(
' atc [options] path'
);
this.log('Options:\n');
this.log([
' -w, --watch',
' [grey]use atc in watch mode (auto compile when file changed)[/grey]',
' -d, --define-syntax',
' [grey]use this if the template files are using simplified template syntax[/grey]',
' -c charset, --charset charset',
' [grey]charset, utf-8 by default[/grey]',
' -o path, --output path',
' [grey]defining an output directory[/grey]',
' --clone-helpers',
' [grey]clone the helper functions to the compiled files, by default, they are seprated[/grey]',
' --version',
' [grey]display the version of atc[/grey]',
' --help',
' [grey]show this help infomation[/grey]'
].join('\n') + '\n\n');
this.log('[grey]Documentation can be found at http://cdc-im.github.io/atc/[/grey]\n');
},
// 过滤不符合命名规范的目录与模板
FILTER_RE: /[^\w\.\-$]/,
// 模板文件后缀
EXTNAME_RE: /\.(html|htm|tpl)$/i,
// 公用的辅助模块名字
HELPERSNAME: '$helpers.js',
/*
* 模板编译引擎
* @param {String} 模板
* @param {String} 外部辅助方法路径(若不定义则会把辅助方法复制后编译到函数内)
* @return {String} 编译好的模板
*/
engine: (function () {
template.isCompress = true;
// 提取include模板
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*include|(?:^|[^$])\binclude\s*\(\s*(["'])(.+?)\1\s*(,\s*(.+?)\s*)?\)/g; //"
var SLASH_RE = /\\\\/g
var parseDependencies = function (code) {
var ret = [];
var uniq = {};
code
.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2 && !uniq.hasOwnProperty(m2)) {
ret.push(m2);
uniq[m2] = true;
}
});
return ret
};
// 包装成RequireJS、SeaJS模块
var toModule = function (code, helpersPath) {
template.onerror = function (e) {
throw e;
};
// 使用artTemplate编译模板
var render = template.compile(code);
var prototype = render.prototype;
render = render.toString()
.replace(/^function\s+(anonymous)/, 'function');
// SeaJS与RequireJS规范,相对路径前面需要带“.”
var fixPath = function (dir) {
dir = dir
.replace(/\\/g, '/')
.replace(/\.js$/, '');
if (!/^(\.)*?\//.test(dir)) {
dir = './' + dir;
}
return dir;
};
var dependencies = [];
parseDependencies(render).forEach(function (file) {
dependencies.push(
'\'' + file + '\': ' + 'require(\'' + fixPath(file) + '\')'
);
});
var isDependencies = dependencies.length;
dependencies = '{' + dependencies.join(',') + '}';
// 输出辅助方法
var helpers;
if (helpersPath) {
helpersPath = fixPath(helpersPath);
helpers = 'require(\'' + helpersPath + '\')';
} else {
helpers = [];
for (var name in prototype) {
if (name !== '$render') {
helpers.push(
'\'' + name + '\': ' + prototype[name].toString()
);
}
}
helpers = '{' + helpers.join(',') + '}';
}
code =
'define(function(require){'
+ (isDependencies ? 'var dependencies=' + dependencies + ';' : '')
+ 'var helpers = ' + helpers + ';'
+ (isDependencies ? 'var $render=function(id,data){'
+ 'return dependencies[id](data);'
+ '};' : '')
+ 'var Render=' + render + ';'
+ 'Render.prototype=helpers;'
+ 'return function(data){'
+ (isDependencies ? 'helpers.$render=$render;' : '')
+ 'return new Render(data) + \'\';'
+ '}'
+ '})';
return code;
};
return function (source, helpersPath) {
return toModule(source, helpersPath);
}
})(),
/** 格式化JS */
format: function(code) {
if (typeof js_beautify !== 'undefined') {
js_beautify =
typeof js_beautify === 'function'
? js_beautify
: js_beautify.js_beautify;
var config = {
indent_size: 4,
indent_char: ' ',
preserve_newlines: true,
braces_on_own_line: false,
keep_array_indentation: false,
space_after_anon_function: true
};
code = js_beautify(code, config);
}
return code;
},
/** 外置辅助方法 */
writeHelpers: function () {
var helpers = [];
var fullname = this._output + '/' + this.HELPERSNAME;
var prototype = template.prototype;
for (var name in prototype) {
if (name !== '$render') {
helpers.push('\'' + name + '\': ' + prototype[name].toString());
}
}
helpers = '{\n' + helpers.join(',\n') + '}';
var module = 'define(function () {'
+ 'return ' + helpers
+ '});'
module = this.format(module);
this._fsWrite(fullname, module);
},
/**
* 在控制台显示日志(支持UBB)
* @param {String} 消息
*/
log: function (message) {
var styles = {
// styles
'bold' : ['\x1B[1m', '\x1B[22m'],
'italic' : ['\x1B[3m', '\x1B[23m'],
'underline' : ['\x1B[4m', '\x1B[24m'],
'inverse' : ['\x1B[7m', '\x1B[27m'],
// colors
'white' : ['\x1B[37m', '\x1B[39m'],
'grey' : ['\x1B[90m', '\x1B[39m'],
'black' : ['\x1B[30m', '\x1B[39m'],
'blue' : ['\x1B[34m', '\x1B[39m'],
'cyan' : ['\x1B[36m', '\x1B[39m'],
'green' : ['\x1B[32m', '\x1B[39m'],
'magenta' : ['\x1B[35m', '\x1B[39m'],
'red' : ['\x1B[31m', '\x1B[39m'],
'yellow' : ['\x1B[33m', '\x1B[39m']
};
styles['b'] = styles['bold'];
styles['i'] = styles['italic'];
styles['u'] = styles['underline'];
message = message.replace(/\[([^\]]*?)\]/igm, function ($1, $2) {
return $2.indexOf('/') === 0
? styles[$2.slice(1)][1]
: styles[$2][0];
});
process.stdout.write(message);
},
// 绑定文件监听事件
_onwatch: function (dir, callback) {
var that = this;
var watchList = {};
var timer = {};
function walk (dir) {
fs.readdirSync(dir).forEach(function (item) {
var fullname = dir + '/' + item;
if (fs.statSync(fullname).isDirectory()){
watch(fullname);
walk(fullname);
}
});
};
// 排除“.”、“_”开头或者非英文命名的目录
function filter (name) {
return !that.FILTER_RE.test(name);
};
function watch (parent) {
var target = path.basename(parent);
if (!filter(target)){
return;
}
if (watchList[parent]) {
watchList[parent].close();
}
watchList[parent] = fs.watch(parent, function (event, filename) {
var fullname = parent + '/' + filename;
var type;
var fstype;
if (!filter(filename)) {
return;
}
// 检查文件、目录是否存在
if (!fs.existsSync(fullname)) {
// 如果目录被删除则关闭监视器
if (watchList[fullname]) {
fstype = 'directory';
watchList[fullname].close();
delete watchList[fullname];
} else {
fstype = 'file';
}
type = 'delete';
} else {
// 文件
if (fs.statSync(fullname).isFile()) {
fstype = 'file';
type = event == 'rename' ? 'create' : 'updated'
// 文件夹
} else if (event === 'rename') {
fstype = 'directory';
type = 'create'
watch(fullname);
walk(fullname);
}
}
var eventData = {
type: type,
target: filename,
parent: parent,
fstype: fstype
};
if (/windows/i.test(require('os').type())) {
// window 下 nodejs fs.watch 不稳定(nodejs v0.10.5)
clearTimeout(timer[fullname]);
timer[fullname] = setTimeout(function() {
callback(eventData);
}, 16);
} else {
callback(eventData);
}
});
};
watch(dir);
walk(dir);
},
// 定义模板语法
_defineSyntax: function () {
var file = __dirname + '/lib/template-syntax.js';
var code = fs.readFileSync(file, 'utf-8');
eval(code);
},
// 筛选模板文件
_filter: function (name) {
return !this.FILTER_RE.test(name) && this.EXTNAME_RE.test(name);
},
// 模板文件写入
_fsWrite: function (file, data) {
this._fsMkdir(path.dirname(file));
return fs.writeFileSync(file, data, this.options['charset']);
},
// 模板文件读取
_fsRead: function (file) {
return fs.readFileSync(file, this.options['charset']);
},
// 创建目录
_fsMkdir: function (dir) {
var currPath = dir;
var toMakeUpPath = [];
while (!fs.existsSync(currPath)) {
toMakeUpPath.unshift(currPath);
currPath = path.dirname(currPath);
}
toMakeUpPath.forEach(function (pathItem) {
fs.mkdirSync(pathItem);
});
},
// 删除模板文件
_fsUnlink: function (file) {
file = file.replace(this._path , this._output);
return fs.existsSync(file) && fs.unlink(file);
},
/** 监听模板的修改进行即时编译 */
watch: function () {
var that = this;
// 监控模板目录
this._onwatch(this._path , function (event) {
var type = event.type;
var fstype = event.fstype;
var target = event.target;
var parent = event.parent;
var fullname = parent + '/' + target;
if (target && fstype === 'file' && that._filter(target)) {
//console.log(type + ': ' + fullname);
if (type === 'delete') {
var js = fullname.replace(that.EXTNAME_RE, '.js')
.replace(that._path , that._output);
that._fsUnlink(js);
} else
if (type === 'updated' || type === 'create') {
that.compile(fullname);
}
}
});
this.log('\n[inverse]Watch..[/inverse]\n\n');
},
/**
* 编译单个模板
* @param {String} 文件
* @return {Boolean} 成功true, 失败false
*/
compile: function (file) {
var name = null;
var that = this;
var success = true;
// 计算辅助方法模块的相对路径
if (!this.options['cloneHelpers']) {
name = this.HELPERSNAME;
var dirname = path.dirname(file);
var join = path.join(this._path , name);
name = path.relative(dirname, join);
}
var source = this._fsRead(file);
var target = file
.replace(this.EXTNAME_RE, '.js')
.replace(this._path , this._output);
var info = file.replace(this._path, '');
var info_source = this.options['path'] + info;
var info_output = this.options['output'] + info.replace(this.EXTNAME_RE, '.js');
this.log('Compile: [green]' + info_source + '[/green]');
try {
var code = this.engine(source, name);
code = this.format(code);
this._fsWrite(target, code);
this.log('[grey] > ' + info_output + '[/grey]\n');
} catch (e) {
this.log(' [inverse][red]' + e.name + '[/red][/inverse]\n');
success = false;
this._debug(e.temp);
process.exit(1);
}
return success;
},
// 调试语法错误
_debug: function (code) {
var code = this.format(code);
var debugFile = this._output + '/.debug.js';
try {
this._fsWrite(debugFile, code);
require(debugFile);
} catch (e) {
console.log(code);
this._fsUnlink(debugFile);
}
},
/** 编译模板目录所有的模板 */
compileAll: function () {
var that = this;
var success = true;
var walk = function (dir) {
var dirList = fs.readdirSync(dir);
success && dirList.forEach(function (item) {
if (fs.statSync(dir + '/' + item).isDirectory()) {
walk(dir + '/' + item);
} else if (that._filter(item)) {
success = that.compile(dir + '/' + item);
}
});
};
walk(this._path );
},
init: function () {
var that = this;
var options = this.options;
// 分析命令行参数
var v;
var args = process.argv.slice(2);
while (args.length > 0) {
v = args.shift();
switch (v) {
// 监控修改
case '-w':
case '--watch':
options.watch = true;
break;
// 让每个输出的模块内嵌辅助方法
case '--clone-helpers':
options.cloneHelpers = true;
break;
// 加载模板语法设置
case '-d':
case '--define-syntax':
options.defineSyntax = true;
break;
// 输出目录
case '-o':
case '--output':
options.output = args.shift();
break;
// 模板编码
case '-c':
case '--charset':
options.charset = args.shift().toLowerCase();
break;
// 版本号
case '--version':
return this.log(this.version + '\n');
break;
// 显示帮助
case '--help':
return this.help();
break;
// 模板目录
default:
if (v) {
options.path = v;
}
break;
}
}
if (!options['path']) {
this.help();
return process.exit(1);
}
if (!fs.existsSync(options['path'])) {
console.log('Error: directory does not exist');
this.help();
return process.exit(1);
};
options['path'] = options['path'].replace(/[\/\\]$/, '');
options['output'] = (options['output'] || options['path']).replace(/[\/\\]$/, '');
// 转换成绝对路径
this._path = path.resolve(options['path']).replace(/[\/\\]$/, '');
this._output = path.resolve(options['output']).replace(/[\/\\]$/, '');
// 加载引擎语法扩展
options['defineSyntax'] && this._defineSyntax();
// 输出公用模块
!options['cloneHelpers'] && this.writeHelpers();
if (options['watch']) {
// 监控模板修改进行即时编译
this.watch();
} else {
// 编译目录中所有模板
this.compileAll();
}
}
};
compiler.init();
//module.exports = compiler;