fis-parser-freemarker
Version:
freemarker template parser plugin for fis
340 lines (296 loc) • 9.37 kB
JavaScript
const defaults = require('../config');
const Freemarker = require('freemarker.js'),
fs = require('fs'),
path = require('path'),
util = fis.util,
projectPath = path.resolve('.');
class FreemarkerParser {
constructor(content, file, settings) {
this.content = content;
this.file = file;
// 配置文件读取
this.options = Object.assign({}, defaults, settings);
this.options.commonMock = this.getAbsolutePath(this.options.commonMock, this.options.root);
// 本地变量初始化
this.ftlFiles = [];
this.jsFiles = [];
this.jsBlocks = [];
this.cssFiles = [];
this.cssBlocks = [];
this.mockFiles = [];
this.framework = '';
}
/**
* 对文件内容进行渲染
*/
renderTpl() {
let options = this.options,
file = this.file,
context = {},
result = this.content;
if (result === '') return result;
// 获取include引入的文件
result = this.compileInclude(result);
// 处理 spring.ftl
result = options.parse ? this.compileSpring(result) : result;
// 获取所有依赖的context
context = this.getContext();
const tmpFile = this.tmpSave(result, file);
// 得到解析后的文件内容
result = options.parse ? new Freemarker({
viewRoot: options.root,
options: {}
}).renderSync(tmpFile, context) : result;
if (result.stderr) {
throw new Error(result.stdout.toString());
} else {
fs.unlinkSync(path.join(options.root, tmpFile));
}
// 添加依赖到输入内容
result = this.addStatics(result);
// 添加依赖缓存,用于同步更新
this.addDeps();
return result;
}
/**
* 读取所有依赖的mock文件,并加入页面依赖缓存,用于同步更新
* @return
* [Object]
*/
getContext() {
let context = {},
options = this.options,
arrFiles = [];
// 添加全局mock到context
if (options.commonMock) {
arrFiles.push(options.commonMock);
}
// 将页面文件同名mock文件加入context
let pageMock = this.file.realpathNoExt + '.mock';
if (util.exists(pageMock)) {
arrFiles.push(pageMock);
}
// 将<#include>引入的mock文件加入context
arrFiles = arrFiles.concat(this.mockFiles);
arrFiles.forEach(_file => {
if (_file) {
util.merge(context, require(_file));
delete require.cache[_file];
}
});
return context;
}
// 解析内容中<#include>引入的文件,收集同名css, js, mock文件
compileInclude(content) {
let options = this.options,
root = options.root,
regInclude = /<#include\s*('|")(.*)\1\s*>/g;
content = content.replace(regInclude, (match, quote, uri) => {
let file = this.getAbsolutePath(uri, root);
if (!file) {
throw new Error('can not load:' + uri + ' [' + this.file.subpath + ']');
}
this.ftlFiles.push(file);
let result = util.read(file);
// 收集css文件
[
this.replaceExt(uri, '.css'),
this.replaceExt(uri, '.scss'),
this.replaceExt(uri, '.less'),
this.replaceExt(uri, '.sass')
].forEach(css => {
let absPath = this.getAbsolutePath(css, root);
if (!absPath) return;
// 处理模板引擎的 root 不是项目根目录的情况
absPath = absPath.replace(projectPath, '');
if (this.cssFiles.indexOf(absPath) === -1) {
this.cssFiles.push(absPath);
}
});
// 收集js文件
let _jsFile = this.getAbsolutePath(this.replaceExt(uri, '.js'), root);
if (_jsFile) {
// 处理模板引擎的 root 不是项目根目录的情况
_jsFile = _jsFile.replace(projectPath, '');
if (this.jsFiles.indexOf(_jsFile) === -1) {
this.jsFiles.push(_jsFile);
}
}
// 收集mock文件
let _mockFile = this.getAbsolutePath(this.replaceExt(uri, '.mock'), root);
if (_mockFile && this.mockFiles.indexOf(_mockFile) === -1) {
this.mockFiles.push(_mockFile);
}
// 继续解析<#include>
result = this.compileInclude(result);
// 开启模板解析时,返回文件内容,否则保持原样
if (options.parse) {
return result;
} else {
return match;
}
});
return content;
}
/**
* 处理 spring.ftl
* @param {Sting} content
*/
compileSpring(content) {
var messages = this.options.springMessages || {};
var regExp = /<@spring.message\s+('|")([\w.]+)\1\s?\/>/g;
var regExpArgs = /<@spring.messageArgs\s+('|")([\w.]+)\1\s?\/>/g;
content = content.replace(regExp, function (match, quote, key) {
return messages[key] || '';
});
content = content.replace(regExpArgs, function (match, auote, key) {
return messages[key] || '';
});
return content;
}
/**
* 临时存储文件内容,让编译器编译
* @param {String} content
* @param {Object} file
*/
tmpSave(content, file) {
const { root } = this.options;
let fileName = file.subpath;
fileName = fileName.replace(/\//g, '_') + '.tmp';
fs.writeFileSync(path.resolve(root, fileName), content);
return fileName;
}
/** 替换文件的扩展名
* @example
* replaceExt('/widget/a/a.html', '.css') => '/widget/a/a.css'
*/
replaceExt(pathname, ext) {
return pathname.substring(0, pathname.lastIndexOf('.')) + ext;
}
/**
* 返回文件绝对路径,因为root为数组,所以每个root都得判断一下
* @param file {String} 文件相对路径
* @param root {Array} root目录数组
* @return {String} 返回文件绝对路径或者null
*/
getAbsolutePath(file, root) {
let result = null,
fileName = '';
if(!file || !root) {
return result;
}
fileName = path.join(root, file);
if(util.exists(fileName)) {
result = fileName;
}
return result;
}
/**
* 添加静态资源依赖
*/
addStatics(content) {
let options = this.options,
loader = options.loader || null, // 模块化加载函数名称[requirejs|modjs|seajs]
loadSync = options.loadSync,
root = options.root,
strCss = '',
strFrameWork = '',
strJs = '',
rCssHolder = /<!--\s?WIDGET_CSS_HOLDER\s?-->/,
rFrameWorkHolder = /<!--\s?WIDGET_FRAMEWORK_HOLDER\s?-->/,
rJsHolder = /<!--\s?WIDGET_JS_HOLDER\s?-->/;
// 拼接css文件引入
this.cssFiles.forEach(_uri => {
strCss += '<link rel="stylesheet" href="' + _uri + '">\n';
});
// 拼接内嵌css代码块
if (this.cssBlocks.length > 0) {
strCss += '<style>\n';
this.cssBlocks.forEach(block => {
strCss += block;
});
strCss += '</style>\n';
}
if (rCssHolder.test(content)) {
content = content.replace(rCssHolder, strCss);
} else {
// css放在</head>标签之前
content = content.replace(/(<\/head>)/i, strCss + '$1');
}
// js modules框架引入
if (this.framework !== '') {
strFrameWork = '<script data-loader src="' + this.framework + '"></script>\n';
if (rFrameWorkHolder.test(content)) {
content = content.replace(rFrameWorkHolder, strFrameWork);
} else {
// js放在</body>标签之前
content = content.replace(/(<\/body>)/i, strFrameWork + '$1');
}
}
if (this.jsFiles.length > 0) {
// 非模块化直接拼接script标签
this.jsFiles.forEach(_uri => {
strJs += '<script src="' + _uri + '"></script>\n';
});
// 模块化加载
if (loader) {
// 如果未开启同步加载,先清空strJs
if (!loadSync) {
strJs = '';
}
switch (loader) {
case 'require':
case 'requirejs':
case 'modjs':
strJs += '<script>require(["' + this.jsFiles.join('","') + '"]);</script>\n';
break;
case 'seajs.use':
case 'seajs':
strJs += '<script>seajs.use(["' + this.jsFiles.join('","') + '"]);</script>\n';
break;
}
}
}
// 拼接内嵌js代码块
if (this.jsBlocks.length > 0) {
strJs += '<script type="text/javascript">\n';
this.jsBlocks.forEach(block => {
strJs += block;
});
strJs += '</script>\n';
}
if (rJsHolder.test(content)) {
content = content.replace(rJsHolder, strJs);
} else {
// js放在</body>标签之前
content = content.replace(/(<\/body>)/i, strJs + '$1');
}
return content;
}
/*
* 将所有引入的ftl和mock文件加入依赖缓存,用于文件修改时,自动编译
*/
addDeps() {
let options = this.options,
file = this.file,
arr = [];
// 添加全局mock到context
if (options.commonMock) {
arr.push(options.commonMock);
}
// 将页面文件同名mock文件加入context
let pageMock = this.file.realpathNoExt + '.mock';
if (util.exists(pageMock)) {
arr.push(pageMock);
}
arr = arr.concat(this.ftlFiles);
arr = arr.concat(this.mockFiles);
arr.forEach(_uri => {
_uri && file.cache.addDeps(_uri);
});
}
}
module.exports = function (content, file, settings) {
return new FreemarkerParser(content, file, settings);
};