UNPKG

ktemplate

Version:

简单支持预编译的 js 模版

286 lines (273 loc) 10.4 kB
/** * @date 12-12-26 * @describe: 简单的js模版 ,支持sourceMap的生成 * @author: KnightWu * @version: 0.0.1 */ var replaces = ["htmlCode='';", "htmlCode+=", ";", "htmlCode"]; var SYNTAXSIGN = {start: '<%', end: '%>', val: '=', dval: '-', sub: '<%=='}; var moduleName = 'tools'; //与 moduleTemplate.js 中变量一致 var fs = require('fs'); var sourceMap = require('source-map'); var path = require('path'); var error = require('./info'); var ss = require('./sourceScaner'); require('./sourceMapExtension'); var suffix = '.html'; var templateToJsGenerator = null; var subToCombinedTemplateNode = null; var currentIndex = {}; var sourceRoot = null; var fileRealPath = null; var resetAll = function () { currentIndex = {}; templateToJsGenerator = null; subToCombinedTemplateNode = new sourceMap.SourceNode(null, null, null); sourceRoot = null; fileRealPath = null; }; /** * 编译模版代码 * @param code html模版代码 * @param helperName html模版帮助代码 * @param isEs6 是否使用es6模式 * @return {Function} 模版函数,接收一个数据,返回html代码 */ var compile = function (code, helperName, isEs6) { isEs6 = !!isEs6; //默认false var tempCode = ''; var fileName = path.basename(fileRealPath); //文件名 if (!!isEs6) { tempCode += '"es6insertposition";\n'; } tempCode += 'var ' + replaces[0] + '\n'; if (!isEs6) { tempCode += 'with(_data||{}){\n'; //添加with支持,缩短取值路径 } ss.init(function (original, generated) { generated.line.skip(2); }); templateToJsGenerator = new sourceMap.SourceMapGenerator({ file: path.basename(sourceRoot) + '.js', //以文件夹名称作为文件名 sourceRoot: sourceRoot //模版文件的文件夹地址 }); // html与逻辑语法分离 code.split(SYNTAXSIGN.start).forEach(function (codeSnip) { var codeArr = codeSnip.split(SYNTAXSIGN.end); var htmlCode = codeArr[0]; var jsCode = codeArr[1]; if (codeArr.length === 1 && htmlCode !== '') { tempCode += html(htmlCode, fileName); } else { tempCode += js(htmlCode, fileName, helperName); if (typeof jsCode !== 'undefined' && jsCode !== '') { tempCode += html(jsCode, fileName); } } }); if (!isEs6) { tempCode += '}\n'; //添加with支持,缩短取值路径 } tempCode += 'return ' + replaces[3] + replaces[2]; var tmplFunction = function error() { }; try { tmplFunction = new Function('_data', tempCode); } catch (err) { error.log(err); error.log(tempCode); } return tmplFunction; }; /** * 处理 HTML 语句,直接将html代码编码,可以忽略特殊字符的影响. * 并且将各系统中的换行统一处理成 \n 的形式。 * @param code html模版中的代码 * @param fileName html模版文件名 * @return {string} 处理后生成的html代码 */ var html = function (code, fileName) { var handledCode = code .replace(/('|"|\\)/g, '\\$1') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\\r\\n/g, '\\n') //处理windows中的换行为unix中通用的换行 .replace(/^(\\r)+(\s)*(\\r)+/g, '\n') .replace(/^(\\n)+(\s)*(\\n)+/g, '\n') .replace(/(\\n|\\r)+(\s)*$/g, '') .replace(/\n/g, '\\n'); ss.consume(code, !!(handledCode !== ''), function (map) { map.source = fileName; templateToJsGenerator.addMapping(map); }, false); return (handledCode !== '') ? replaces[1] + "'" + handledCode + "'" + replaces[2] + '\n' : ''; }; /** * 处理逻辑语句 * @param code html模版中的代码 * @param fileName html模版文件 * @param helperName html模版帮助js方法 * @return {string} 处理后的js代码 */ var js = function (code, fileName, helperName) { ss.consume(code, true, function (map) { map.source = fileName; templateToJsGenerator.addMapping(map); }, true); var handledCode = code; if (code.indexOf(SYNTAXSIGN.val) === 0) { //是否值 handledCode = replaces[1] + code.substring(1).replace(/[\s;]*$/, '') + replaces[2]; } if (code.indexOf(SYNTAXSIGN.dval) === 0) { handledCode = replaces[1] + (helperName || moduleName) + '.escape(' + code.substring(1).replace(/[\s;]*$/, '') + ')' + replaces[2]; } return handledCode + '\n'; }; /** * 生成sourceMap * sourceMap中指定的 sourceRoot 是指:在运行时,相对于当前运行页面的路径。所以需要动态的计算sourceRoot的值 * @param runRoot 运行时路径 */ var generateMap = function (runRoot) { var relRoot = './'; //默认的 sourceRoot,相当于当前运行文件的路径 if (runRoot !== undefined) { //如果指定了运行路径 relRoot = path.relative(runRoot, templateToJsGenerator._sourceRoot);// 计算运行路径和源文件位置的相对路径,作为最终map的sourceRoot } var fileName = templateToJsGenerator._file; var A2C = new sourceMap.SourceMapGenerator({ file: fileName, sourceRoot: relRoot }); A2C.combine(subToCombinedTemplateNode, templateToJsGenerator, fileName); return A2C; }; /** * 代码对应的 sourceNode 数组 * @param sourcePath 路径 * @param code 代码 * @return {Array} 返回sourceNode数组 */ var sourceToSourceNode = function (sourcePath, code) { var enter = '\r\n'; var codeNodes = code.split(enter); var sourceNodes = [], cIndex = currentIndex[sourcePath] || 1; var fileName = path.relative(sourceRoot, sourcePath); for (var i = 0, length = codeNodes.length; i < length; i++) { sourceNodes.push(new sourceMap.SourceNode( i + cIndex, 0, fileName, codeNodes[i] + ((length - 1 === i) ? '' : enter)) ); } if (currentIndex[sourcePath] === undefined) { currentIndex[sourcePath] = length; } else { currentIndex[sourcePath] = currentIndex[sourcePath] + length - 1; } return sourceNodes; }; /** * 把注释处理成空格和换行 * todo:多行的文本正则无法处理,用____这种比较挫的方式代替 * @param code */ var handleComments = function (code) { var comments = /<!--.*?--[\s]*>/g; var stCode = code.replace(/\r\n|\n/g, '@#$____$#@'); stCode = stCode.replace(comments, function (fragment) { var codes = fragment.split('@#$____$#@'); var lines = codes.length; var column = codes[lines - 1].length + 1; var linesArr = (new Array(lines)).join('\n'); var columnArr = (new Array(column)).join(' '); return linesArr + columnArr; }); stCode = stCode.replace(/@#\$____\$#@/g, '\n'); return stCode; }; /** * 将子模版合并到父模版中 * @param code 合成后的html模版代码 * @param tPath html模版地址 * todo:正则有问题 * @return {*} 最终返回此模版的完整html模版代码 */ var resolveSub = function (code, tPath) { code = handleComments(code); var codeChunk = new RegExp(SYNTAXSIGN.sub + '[\\s]*([^' + SYNTAXSIGN.end + '(]+)\\(([^' + SYNTAXSIGN.end + ']+)\\)[;\\s]*' + SYNTAXSIGN.end, 'g'); var result = codeChunk.test(code.toString()); if (result) { var subPath = RegExp.$1, dataArgument = RegExp.$2; var leftContext = RegExp.leftContext; var rightContext = RegExp.rightContext; subPath = path.resolve(path.dirname(tPath), subPath + suffix); subToCombinedTemplateNode.add(sourceToSourceNode(tPath, leftContext)); var subTemplateCode = getSubTemplateCode(subPath, dataArgument); if (subTemplateCode.indexOf(SYNTAXSIGN.sub) !== -1) { subTemplateCode = resolveSub(subTemplateCode, subPath); } else { subToCombinedTemplateNode.add(sourceToSourceNode(subPath, subTemplateCode)); } if (rightContext.indexOf(SYNTAXSIGN.sub) !== -1) { resolveSub(rightContext, tPath); } else { subToCombinedTemplateNode.add(sourceToSourceNode(tPath, rightContext)); } } else { //无子模版 subTemplateCode = subToCombinedTemplateNode.add(sourceToSourceNode(tPath, code)); } return subTemplateCode; }; /** * 获取子模块 html 模版代码 * @param subPath 子模版ID号 * @param dataArg 数据参数 * @return {string} 返回此子模版的html模版代码 */ var getSubTemplateCode = function (subPath, dataArg) { return SYNTAXSIGN.start + '(function(_data){' + SYNTAXSIGN.end + fs.readFileSync(subPath, 'utf-8') + SYNTAXSIGN.start + '})(' + dataArg + ');' + SYNTAXSIGN.end; }; /** * 编译html模版代码 * @param templatePath 主模版的路径,模版可能由多个子模版构成 * @param helperName 帮助模块名称 * @param isEs6 是否es6模式 * @return {Function} 模版代码生成的js函数 */ exports.compile = function (templatePath, helperName, isEs6) { resetAll(); fileRealPath = path.resolve(templatePath); // 模版主文件的真实地址 sourceRoot = path.dirname(fileRealPath); // 模版主文件的文件夹地址 resolveSub(fs.readFileSync(templatePath, 'utf-8'), templatePath); return compile(subToCombinedTemplateNode.toString(), helperName, isEs6); }; /** * 生成sourceMap * @param generatedFilePath 与 sourceMap 相关联的生成文件的路径, * 其中文件名应是主模版文件夹名,在compile方法中生成 * @param runRoot 运行时路径 * @param handleMap 处理生成的map,没有则不变化 * @return {*} */ exports.generateSourceMap = function (generatedFilePath, runRoot, handleMap) { var realPath = path.resolve(generatedFilePath); //最终生成的文件的路径 var mapPath = realPath + '.map'; // map文件路径 var templateSourceMap = generateMap(runRoot); if (handleMap !== undefined) { templateSourceMap = handleMap(generateMap(runRoot)); } fs.writeFileSync(mapPath, templateSourceMap.toString()); //和生成的模版文件同路径下,写map文件 fs.appendFileSync(realPath, '\n//# sourceMappingURL=' + path.basename(mapPath)); //源文件连上map文件 }; exports.directCompile = compile; /** * 初始化语法符号 * @param config */ exports.initSyntax = function (config) { SYNTAXSIGN = config; };