mm_tpl 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +870 -37
- package/README_EN.md +881 -38
- package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +165 -0
- package/doc//350/256/276/350/256/241/346/200/273/347/273/223.md +381 -0
- package/index.js +46 -471
- package/lib/asset.js +129 -0
- package/lib/css.js +34 -0
- package/lib/engine.js +1256 -0
- package/lib/hook.js +302 -0
- package/lib/js.js +34 -0
- package/lib/lang.js +184 -0
- package/lib/memory.js +103 -0
- package/lib/sqlite.js +176 -0
- package/lib/template.js +125 -0
- package/lib/tpl_rule.js +288 -0
- package/lib/util.js +79 -0
- package/package.json +15 -3
- package/hook.js +0 -302
- package/tpl_rule.js +0 -287
package/lib/sqlite.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const {
|
|
2
|
+
DatabaseSync
|
|
3
|
+
} = require('node:sqlite');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQLite 存储
|
|
7
|
+
*
|
|
8
|
+
* 与内存存储(Memory)实现同一契约、方法一一对应,数据持久化到 SQLite 文件,
|
|
9
|
+
* 通过 new Tpl({ user_store: new Sqlite('tpl.db') }) 注入即可替换默认存储。
|
|
10
|
+
*
|
|
11
|
+
* 偏好按固定列存放(style / lang),写入时只更新本次传入的列,不触碰其他列,
|
|
12
|
+
* 且由单条 upsert 完成,无需读出整条再写回,故改一个字段不会影响另一个字段,
|
|
13
|
+
* 多个连接并发写同一文件时也不会互相覆盖。列可直接用于统计,例如
|
|
14
|
+
* SELECT style, COUNT(*) FROM users GROUP BY style。
|
|
15
|
+
*
|
|
16
|
+
* 标识一律按字符串入表,故数字 1001 与字符串 '1001' 视为同一键。
|
|
17
|
+
* 依赖 Node 内置的 node:sqlite(Node 22.5 起提供),低版本无法加载本模块。
|
|
18
|
+
*
|
|
19
|
+
* 初始化时若表不存在则自动建表。
|
|
20
|
+
*
|
|
21
|
+
* 存储契约(store):
|
|
22
|
+
* getUser(user_id) -> Object|null
|
|
23
|
+
* setUser(user_id, prefs) -> Object
|
|
24
|
+
* delUser(user_id) -> Boolean
|
|
25
|
+
* hasUser(user_id) -> Boolean
|
|
26
|
+
* listUsers() -> Array<String>
|
|
27
|
+
* clear() -> Sqlite
|
|
28
|
+
* @class
|
|
29
|
+
*/
|
|
30
|
+
class Sqlite {
|
|
31
|
+
/**
|
|
32
|
+
* 构造函数
|
|
33
|
+
* @param {String} file - 数据库文件路径,缺省为 ':memory:'(进程退出即丢失)
|
|
34
|
+
*/
|
|
35
|
+
constructor(file = ':memory:') {
|
|
36
|
+
/**
|
|
37
|
+
* 数据库连接
|
|
38
|
+
*/
|
|
39
|
+
this._db = new DatabaseSync(file);
|
|
40
|
+
this._createTable();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 创建用户偏好表(私有方法)
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
Sqlite.prototype._createTable = function () {
|
|
49
|
+
this._db.exec(
|
|
50
|
+
'CREATE TABLE IF NOT EXISTS users (' +
|
|
51
|
+
'user_id TEXT PRIMARY KEY NOT NULL, ' +
|
|
52
|
+
'style TEXT, ' +
|
|
53
|
+
'lang TEXT' +
|
|
54
|
+
');'
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 写入用户偏好
|
|
60
|
+
* 同一用户重复写入时按字段合并:本次传入的列取新值,未传入的列保持原值
|
|
61
|
+
* @param {String|Number} user_id - 用户标识
|
|
62
|
+
* @param {Object} prefs - 用户偏好,形如 { style, lang }
|
|
63
|
+
* @returns {Object} 保存后的偏好副本
|
|
64
|
+
*/
|
|
65
|
+
Sqlite.prototype.setUser = function (user_id, prefs) {
|
|
66
|
+
var data = prefs || {};
|
|
67
|
+
this._db
|
|
68
|
+
.prepare(
|
|
69
|
+
'INSERT INTO users(user_id, style, lang) VALUES(?, ?, ?) ' +
|
|
70
|
+
'ON CONFLICT(user_id) DO UPDATE SET ' +
|
|
71
|
+
'style = COALESCE(excluded.style, users.style), ' +
|
|
72
|
+
'lang = COALESCE(excluded.lang, users.lang)'
|
|
73
|
+
)
|
|
74
|
+
.run(this._key(user_id), this._val(data.style), this._val(data.lang));
|
|
75
|
+
return this.getUser(user_id);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 读取用户偏好
|
|
80
|
+
* 只返回已登记的字段,未登记时返回 null
|
|
81
|
+
* @param {String|Number} user_id - 用户标识
|
|
82
|
+
* @returns {Object|null} 用户偏好副本
|
|
83
|
+
*/
|
|
84
|
+
Sqlite.prototype.getUser = function (user_id) {
|
|
85
|
+
var row = this._db
|
|
86
|
+
.prepare('SELECT style, lang FROM users WHERE user_id = ?')
|
|
87
|
+
.get(this._key(user_id));
|
|
88
|
+
if (!row) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
var data = {};
|
|
92
|
+
if (row.style != null) {
|
|
93
|
+
data.style = row.style;
|
|
94
|
+
}
|
|
95
|
+
if (row.lang != null) {
|
|
96
|
+
data.lang = row.lang;
|
|
97
|
+
}
|
|
98
|
+
return data;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 删除用户偏好
|
|
103
|
+
* @param {String|Number} user_id - 用户标识
|
|
104
|
+
* @returns {Boolean} 是否删除了已登记的用户
|
|
105
|
+
*/
|
|
106
|
+
Sqlite.prototype.delUser = function (user_id) {
|
|
107
|
+
var result = this._db
|
|
108
|
+
.prepare('DELETE FROM users WHERE user_id = ?')
|
|
109
|
+
.run(this._key(user_id));
|
|
110
|
+
return result.changes > 0;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 判断用户是否已登记
|
|
115
|
+
* @param {String|Number} user_id - 用户标识
|
|
116
|
+
* @returns {Boolean} 是否已登记
|
|
117
|
+
*/
|
|
118
|
+
Sqlite.prototype.hasUser = function (user_id) {
|
|
119
|
+
var row = this._db
|
|
120
|
+
.prepare('SELECT 1 AS found FROM users WHERE user_id = ?')
|
|
121
|
+
.get(this._key(user_id));
|
|
122
|
+
return Boolean(row);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 列出已登记的用户标识
|
|
127
|
+
* @returns {Array<String>} 用户标识数组,按登记先后排列
|
|
128
|
+
*/
|
|
129
|
+
Sqlite.prototype.listUsers = function () {
|
|
130
|
+
var rows = this._db
|
|
131
|
+
.prepare('SELECT user_id FROM users ORDER BY rowid')
|
|
132
|
+
.all();
|
|
133
|
+
return rows.map(function (row) {
|
|
134
|
+
return row.user_id;
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 清空全部用户偏好
|
|
140
|
+
* @returns {Sqlite} 当前实例
|
|
141
|
+
*/
|
|
142
|
+
Sqlite.prototype.clear = function () {
|
|
143
|
+
this._db.prepare('DELETE FROM users').run();
|
|
144
|
+
return this;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 关闭数据库连接
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
Sqlite.prototype.close = function () {
|
|
152
|
+
this._db.close();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 统一存储键的表示(私有方法)
|
|
157
|
+
* @param {String|Number} user_id - 用户标识
|
|
158
|
+
* @returns {String} 文本标识
|
|
159
|
+
*/
|
|
160
|
+
Sqlite.prototype._key = function (user_id) {
|
|
161
|
+
return String(user_id);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 统一列值的绑定表示(私有方法)
|
|
166
|
+
* 未传入的字段绑定 null,交由 upsert 的 COALESCE 保留原值
|
|
167
|
+
* @param {String|undefined} value - 字段值
|
|
168
|
+
* @returns {String|null} 待绑定的值
|
|
169
|
+
*/
|
|
170
|
+
Sqlite.prototype._val = function (value) {
|
|
171
|
+
return (typeof (value) === 'string') ? value : null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
Sqlite
|
|
176
|
+
};
|
package/lib/template.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板文件加载器
|
|
3
|
+
* 模板按目录优先级覆盖:高优先级目录存在同名文件时覆盖低优先级目录的同名文件
|
|
4
|
+
* 模板目录链由引擎按「自定义目录、当前主题、默认主题」给出,主题目录下按功能划分,如 static/template/{主题}/article/list.html
|
|
5
|
+
* 组件目录链同构,只是每级多一个固定的组件子目录前缀,如 static/template/{主题}/component/bar.html
|
|
6
|
+
* @class
|
|
7
|
+
*/
|
|
8
|
+
class Template {
|
|
9
|
+
/**
|
|
10
|
+
* 创建模板文件加载器
|
|
11
|
+
* @param {Object} tpl - 所属模板实例
|
|
12
|
+
*/
|
|
13
|
+
constructor(tpl) {
|
|
14
|
+
/**
|
|
15
|
+
* 所属模板实例
|
|
16
|
+
*/
|
|
17
|
+
this.tpl = tpl;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 检查主题目录是否存在
|
|
23
|
+
* @param {string} theme - 主题名称
|
|
24
|
+
* @returns {boolean} 主题目录是否存在
|
|
25
|
+
*/
|
|
26
|
+
Template.prototype.hasTheme = function (theme) {
|
|
27
|
+
if (!theme) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return this.tpl.getThemeDir(theme).hasDir();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 检查风格目录是否存在
|
|
35
|
+
* 当前主题或默认主题下存在该风格即视为有效
|
|
36
|
+
* @param {string} style - 风格名称
|
|
37
|
+
* @returns {boolean} 风格目录是否存在
|
|
38
|
+
*/
|
|
39
|
+
Template.prototype.hasStyle = function (style) {
|
|
40
|
+
if (!style) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
var tpl = this.tpl;
|
|
44
|
+
var cg = tpl.config;
|
|
45
|
+
var themes = [tpl.current_theme];
|
|
46
|
+
if (cg.default_theme && cg.default_theme !== tpl.current_theme) {
|
|
47
|
+
themes.push(cg.default_theme);
|
|
48
|
+
}
|
|
49
|
+
for (var i = 0; i < themes.length; i++) {
|
|
50
|
+
if (themes[i] && tpl.getStyleDir(themes[i], style).hasDir()) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 补全文件扩展名
|
|
59
|
+
* 入参已带其他扩展名或已带模板扩展名时保持原样
|
|
60
|
+
* @param {string} file - 文件名
|
|
61
|
+
* @returns {string} 补全后的文件名
|
|
62
|
+
*/
|
|
63
|
+
Template.prototype._fixExt = function (file) {
|
|
64
|
+
var cg = this.tpl.config;
|
|
65
|
+
if (file.replace('./', '').indexOf('.') === -1 && !file.endsWith(cg.extname)) {
|
|
66
|
+
return file + cg.extname;
|
|
67
|
+
}
|
|
68
|
+
return file;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 按目录优先级搜索文件
|
|
73
|
+
* @param {string} file - 文件名
|
|
74
|
+
* @param {Array<string>} dirs - 目录列表,顺序即优先级,为空时视为未命中
|
|
75
|
+
* @returns {string} 命中的文件全路径;未命中时返回最后一个候选路径,其 hasFile() 为 false
|
|
76
|
+
*/
|
|
77
|
+
Template.prototype._locate = function (file, dirs) {
|
|
78
|
+
if (!dirs.length) {
|
|
79
|
+
// 返回不存在的路径,交由调用方按 hasFile() 判断
|
|
80
|
+
return file.fullname(this.tpl.dir) + '.not-exist';
|
|
81
|
+
}
|
|
82
|
+
var f = file.fullname(dirs[0]);
|
|
83
|
+
for (var i = 1; i < dirs.length && !f.hasFile(); i++) {
|
|
84
|
+
f = file.fullname(dirs[i]);
|
|
85
|
+
}
|
|
86
|
+
return f;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取模板文件
|
|
91
|
+
* 按目录优先级从高到低搜索,返回第一个存在的文件
|
|
92
|
+
* options.dir 指定时只在该目录查找,用于主题体系外的模板
|
|
93
|
+
* @param {string} file - 模板文件路径
|
|
94
|
+
* @param {Object} options - 配置参数,dir 为指定的查找目录
|
|
95
|
+
* @returns {string} 模板文件路径
|
|
96
|
+
*/
|
|
97
|
+
Template.prototype.getFile = function (file, options) {
|
|
98
|
+
if (typeof (file) !== 'string') {
|
|
99
|
+
throw new Error('模板文件路径必须是字符串');
|
|
100
|
+
}
|
|
101
|
+
file = this._fixExt(file);
|
|
102
|
+
var dirs = (options && options.dir) ? [options.dir.fullname(this.tpl.dir)] : this.tpl.getTemplateDirs();
|
|
103
|
+
return this._locate(file, dirs);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取组件文件
|
|
108
|
+
* 组件名相对组件目录解析,省略扩展名时自动补全;名称指向组件目录内的路径,可含子目录,如 article/card
|
|
109
|
+
* 组件名不相对调用方所在文件解析,而是沿组件目录链查找
|
|
110
|
+
* @param {string} name - 组件名称,如 bar、article/card
|
|
111
|
+
* @param {Object} options - 配置参数,dir 为指定的查找目录
|
|
112
|
+
* @returns {string} 组件文件路径
|
|
113
|
+
*/
|
|
114
|
+
Template.prototype.getComponent = function (name, options) {
|
|
115
|
+
if (typeof (name) !== 'string') {
|
|
116
|
+
throw new Error('组件名称必须是字符串');
|
|
117
|
+
}
|
|
118
|
+
var file = this._fixExt(name.replace(/^\.\//, ''));
|
|
119
|
+
var dirs = (options && options.dir) ? [options.dir.fullname(this.tpl.dir)] : this.tpl.getComponentDirs();
|
|
120
|
+
return this._locate(file, dirs);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
Template
|
|
125
|
+
};
|
package/lib/tpl_rule.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 原生模板语法规则
|
|
5
|
+
* @type {Object}
|
|
6
|
+
*/
|
|
7
|
+
var nativeRule = {
|
|
8
|
+
test: /[ \t\r\n]*<!--\[(#?)((?:==|=#|[=-])?)[ \t]*([\w\W]*?)[ \t]*(-?)\]-->/,
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 处理原生模板语法
|
|
12
|
+
* @param {string} match - 匹配的字符串
|
|
13
|
+
* @param {string} comment - 注释标记
|
|
14
|
+
* @param {string} output - 输出类型
|
|
15
|
+
* @param {string} code - 代码内容
|
|
16
|
+
* @returns {Object} 处理结果
|
|
17
|
+
*/
|
|
18
|
+
use: function use(match, comment, output, code /*, trimMode*/) {
|
|
19
|
+
output = {
|
|
20
|
+
'-': 'raw',
|
|
21
|
+
'=': 'escape',
|
|
22
|
+
'': false,
|
|
23
|
+
// v3 compat: raw output
|
|
24
|
+
'==': 'raw',
|
|
25
|
+
'=#': 'raw'
|
|
26
|
+
}[output];
|
|
27
|
+
|
|
28
|
+
// ejs compat: comment tag
|
|
29
|
+
if (comment) {
|
|
30
|
+
code = '/*' + code + '*/';
|
|
31
|
+
output = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ejs compat: trims following newline
|
|
35
|
+
// if (trimMode) {}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
code: code,
|
|
39
|
+
output: output
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 简洁模板语法规则
|
|
46
|
+
* @type {Object}
|
|
47
|
+
*/
|
|
48
|
+
var artRule = {
|
|
49
|
+
// 表达式内允许出现非嵌套的对象字面量,如 ${@$component('bar', { id: 1 })}
|
|
50
|
+
// 若只用 [\w\W]*? 会停在第一个 } 上,把对象字面量截断成语法错误
|
|
51
|
+
test: /[ \t\r\n]*\$\{([@#]?)[ \t]*(\/?)((?:[^{}]|\{[^{}]*\})*?)[ \t]*\}/,
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 处理简洁模板语法
|
|
55
|
+
* @param {string} match - 匹配的字符串
|
|
56
|
+
* @param {string} raw - 原始输出标记
|
|
57
|
+
* @param {string} close - 闭合标记
|
|
58
|
+
* @param {string} code - 代码内容
|
|
59
|
+
* @returns {Object} 处理结果
|
|
60
|
+
*/
|
|
61
|
+
use(match, raw, close, code) {
|
|
62
|
+
var compiler = this;
|
|
63
|
+
var options = compiler.options;
|
|
64
|
+
var esTokens = compiler.getEsTokens(code);
|
|
65
|
+
var values = esTokens.map(function (token) {
|
|
66
|
+
return token.value;
|
|
67
|
+
});
|
|
68
|
+
var result = {};
|
|
69
|
+
|
|
70
|
+
var group = void 0;
|
|
71
|
+
var output = raw ? 'raw' : false;
|
|
72
|
+
var key = close + values.shift();
|
|
73
|
+
|
|
74
|
+
// 旧版语法升级提示
|
|
75
|
+
var warn = function warn(oldSyntax, newSyntax) {
|
|
76
|
+
$.log.warn((options.filename || 'anonymous') + ':' + (match.line + 1) + ':' + (match.start + 1) + '\n' + (
|
|
77
|
+
'Template upgrade: ${' + oldSyntax + '} -> ${' + newSyntax + '}'));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// v3 compat: #value
|
|
81
|
+
if (raw === '#') {
|
|
82
|
+
warn('#value', '@value');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
switch (key) {
|
|
86
|
+
case 'set':
|
|
87
|
+
code = 'var ' + values.join('').trim();
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'if':
|
|
91
|
+
code = 'if(' + values.join('').trim() + '){';
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'else':
|
|
95
|
+
var indexIf = values.indexOf('if');
|
|
96
|
+
|
|
97
|
+
if (~indexIf) {
|
|
98
|
+
values.splice(0, indexIf + 1);
|
|
99
|
+
code = '}else if(' + values.join('').trim() + '){';
|
|
100
|
+
} else {
|
|
101
|
+
code = '}else{';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case '/if':
|
|
107
|
+
code = '}';
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case 'loop':
|
|
111
|
+
case 'each':
|
|
112
|
+
group = artRule._split(esTokens);
|
|
113
|
+
group.shift();
|
|
114
|
+
|
|
115
|
+
if (group[1] === 'as') {
|
|
116
|
+
// ... v3 compat ...
|
|
117
|
+
warn('each object as value index', 'each object value index');
|
|
118
|
+
group.splice(1, 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var object = group[0] || '$data';
|
|
122
|
+
var value = group[1] || '$value';
|
|
123
|
+
var index = group[2] || '$index';
|
|
124
|
+
|
|
125
|
+
code = '$each(' + object + ',function(' + value + ',' + index + '){';
|
|
126
|
+
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case '/loop':
|
|
130
|
+
case '/each':
|
|
131
|
+
code = '})';
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'block':
|
|
135
|
+
group = artRule._split(esTokens);
|
|
136
|
+
group.shift();
|
|
137
|
+
code = 'block(' + group.join(',').trim() + ',function(){';
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case '/block':
|
|
141
|
+
code = '})';
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'echo':
|
|
145
|
+
key = 'print';
|
|
146
|
+
warn('echo value', 'value');
|
|
147
|
+
case 'print':
|
|
148
|
+
case 'include':
|
|
149
|
+
case 'extend':
|
|
150
|
+
if (values.join('').trim().indexOf('(') !== 0) {
|
|
151
|
+
// 执行函数省略 `()` 与 `,`
|
|
152
|
+
group = artRule._split(esTokens);
|
|
153
|
+
group.shift();
|
|
154
|
+
code = key + '(' + group.join(',') + ')';
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
if (~values.indexOf('|')) {
|
|
160
|
+
var v3split = ':'; // ... v3 compat ...
|
|
161
|
+
|
|
162
|
+
// 将过滤器解析成二维数组
|
|
163
|
+
var _group = esTokens.reduce(function (group, token) {
|
|
164
|
+
var value = token.value,
|
|
165
|
+
type = token.type;
|
|
166
|
+
|
|
167
|
+
if (value === '|') {
|
|
168
|
+
group.push([]);
|
|
169
|
+
} else if (type !== 'whitespace' && type !== 'comment') {
|
|
170
|
+
if (!group.length) {
|
|
171
|
+
group.push([]);
|
|
172
|
+
}
|
|
173
|
+
if (value === v3split && group[group.length - 1].length === 1) {
|
|
174
|
+
warn('value | filter: argv', 'value | filter argv');
|
|
175
|
+
} else {
|
|
176
|
+
group[group.length - 1].push(token);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return group;
|
|
180
|
+
}, []).map(function (g) {
|
|
181
|
+
return artRule._split(g);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 将过滤器管道化
|
|
185
|
+
code = _group.reduce(function (accumulator, filter) {
|
|
186
|
+
var name = filter.shift();
|
|
187
|
+
filter.unshift(accumulator);
|
|
188
|
+
|
|
189
|
+
return '$imports.' + name + '(' + filter.join(',') + ')';
|
|
190
|
+
}, _group.shift().join(' ').trim());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
output = output || 'escape';
|
|
194
|
+
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
result.code = code;
|
|
199
|
+
result.output = output;
|
|
200
|
+
return result;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 将多个javascript表达式拆分成组
|
|
205
|
+
* @param {Array} esTokens - ES令牌数组
|
|
206
|
+
* @returns {Array} 分组后的表达式数组
|
|
207
|
+
*/
|
|
208
|
+
_split: function _split(esTokens) {
|
|
209
|
+
esTokens = esTokens.filter(function (_ref) {
|
|
210
|
+
var type = _ref.type;
|
|
211
|
+
|
|
212
|
+
return type !== 'whitespace' && type !== 'comment';
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
var current = 0;
|
|
216
|
+
var lastToken = esTokens.shift();
|
|
217
|
+
var punctuator = 'punctuator';
|
|
218
|
+
var close = /\]|\)/;
|
|
219
|
+
var group = [
|
|
220
|
+
[lastToken]
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
while (current < esTokens.length) {
|
|
224
|
+
var esToken = esTokens[current];
|
|
225
|
+
|
|
226
|
+
if (esToken.type === punctuator || lastToken.type === punctuator && !close.test(lastToken.value)) {
|
|
227
|
+
group[group.length - 1].push(esToken);
|
|
228
|
+
} else {
|
|
229
|
+
group.push([esToken]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lastToken = esToken;
|
|
233
|
+
|
|
234
|
+
current++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return group.map(function (g) {
|
|
238
|
+
return g.map(function (g) {
|
|
239
|
+
return g.value;
|
|
240
|
+
}).join('');
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* HTML注释模板语法规则
|
|
247
|
+
* @type {Object}
|
|
248
|
+
*/
|
|
249
|
+
var htmlRule = Object.assign({}, artRule, {
|
|
250
|
+
test: /[ \t\r\n]*<!--\{([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*\}-->/
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* JS注释模板语法规则
|
|
255
|
+
* @type {Object}
|
|
256
|
+
*/
|
|
257
|
+
var jsRule = Object.assign({}, artRule, {
|
|
258
|
+
test: /[ \t\r\n]*\/\*\[([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*\]\*\//
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Python注释模板语法规则
|
|
263
|
+
* @type {Object}
|
|
264
|
+
*/
|
|
265
|
+
var pyRule = Object.assign({}, artRule, {
|
|
266
|
+
test: /[ \t\r\n]*##([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*##/
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Python三引号注释模板语法规则
|
|
271
|
+
* @type {Object}
|
|
272
|
+
*/
|
|
273
|
+
var pyRule2 = Object.assign({}, artRule, {
|
|
274
|
+
test: /"""#([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*#"""/
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
htmlRule,
|
|
279
|
+
jsRule,
|
|
280
|
+
artRule,
|
|
281
|
+
nativeRule,
|
|
282
|
+
pyRule,
|
|
283
|
+
pyRule2
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 模板工具类
|
|
5
|
+
* 收纳与实例状态无关的纯辅助函数,全部以静态方法提供
|
|
6
|
+
* @class
|
|
7
|
+
*/
|
|
8
|
+
class TplUtil {
|
|
9
|
+
/**
|
|
10
|
+
* 获取文件修改时间
|
|
11
|
+
* @param {string} file - 文件路径
|
|
12
|
+
* @returns {number} 修改时间戳,取不到时返回0
|
|
13
|
+
*/
|
|
14
|
+
static getFileTime(file) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.statSync(file).mtimeMs;
|
|
17
|
+
} catch {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 转义正则表达式特殊字符
|
|
24
|
+
* @param {string} str - 原始字符串
|
|
25
|
+
* @returns {string} 转义后的字符串
|
|
26
|
+
*/
|
|
27
|
+
static escapeRegExp(str) {
|
|
28
|
+
return str.replace('\\', '\\\\').replace('$', '\\$').replace('[', '\\[').replace(']', '\\]')
|
|
29
|
+
.replace('{', '\\{').replace('}', '\\}').replace('/', '\\/').replace('*', '\\*');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 构建字符转义规则
|
|
34
|
+
* 转义时把语法字符替换为中间字符,渲染后再还原,避免与模板语法冲突
|
|
35
|
+
* @param {Array<string>} change_str - 需要转义与还原的字符串列表
|
|
36
|
+
* @returns {Object} 转义规则,含 regex_in、str_in、regex_out、str_out
|
|
37
|
+
*/
|
|
38
|
+
static buildEscapeRule(change_str) {
|
|
39
|
+
var rule = {
|
|
40
|
+
regex_in: [],
|
|
41
|
+
str_in: [],
|
|
42
|
+
regex_out: [],
|
|
43
|
+
str_out: []
|
|
44
|
+
};
|
|
45
|
+
var lt = change_str || [];
|
|
46
|
+
for (var i = 0; i < lt.length; i++) {
|
|
47
|
+
var str = lt[i];
|
|
48
|
+
rule.regex_in.push(new RegExp(TplUtil.escapeRegExp(str), 'gi'));
|
|
49
|
+
var str_in = str.split('').join('·') + '·';
|
|
50
|
+
rule.str_in.push(str_in);
|
|
51
|
+
rule.regex_out.push(new RegExp(TplUtil.escapeRegExp(str_in), 'gi'));
|
|
52
|
+
rule.str_out.push(str.replace('\\', ''));
|
|
53
|
+
}
|
|
54
|
+
return rule;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 转义或还原字符串
|
|
59
|
+
* @param {string} body - 字符串
|
|
60
|
+
* @param {Object} rule - 转义规则
|
|
61
|
+
* @param {boolean} restore - 是否还原
|
|
62
|
+
* @returns {string} 处理后的字符串
|
|
63
|
+
*/
|
|
64
|
+
static escape(body, rule, restore) {
|
|
65
|
+
if (!body) {
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
var rx = restore ? rule.regex_out : rule.regex_in;
|
|
69
|
+
var st = restore ? rule.str_out : rule.str_in;
|
|
70
|
+
for (var i = 0; i < rx.length; i++) {
|
|
71
|
+
body = body.replace(rx[i], st[i]);
|
|
72
|
+
}
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
TplUtil
|
|
79
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_tpl",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"description": "Super Meimei Template Engine - High-performance template engine based on art-template with MVC pattern, hook system, theme switching and caching mechanism",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"io": true,
|
|
7
|
+
"priority": 100,
|
|
6
8
|
"scripts": {
|
|
7
9
|
"dev": "nodemon test.js",
|
|
10
|
+
"demo": "node example/demo.js",
|
|
8
11
|
"test": "node test.js",
|
|
9
12
|
"coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
|
|
10
13
|
},
|
|
@@ -13,6 +16,9 @@
|
|
|
13
16
|
"url": "git+https://gitee.com/qiuwenwu91/mm_tpl.git"
|
|
14
17
|
},
|
|
15
18
|
"keywords": [
|
|
19
|
+
"mm_mod",
|
|
20
|
+
"mm_infra",
|
|
21
|
+
"mm_web",
|
|
16
22
|
"template",
|
|
17
23
|
"engine",
|
|
18
24
|
"mvc",
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
"homepage": "https://gitee.com/qiuwenwu91/mm_tpl#readme",
|
|
35
41
|
"dependencies": {
|
|
36
42
|
"art-template": "^4.13.4",
|
|
37
|
-
"mm_expand": "^2.5.
|
|
43
|
+
"mm_expand": "^2.5.4"
|
|
38
44
|
},
|
|
39
45
|
"engines": {
|
|
40
46
|
"node": ">=12.0.0"
|
|
@@ -43,8 +49,14 @@
|
|
|
43
49
|
"index.js",
|
|
44
50
|
"hook.js",
|
|
45
51
|
"tpl_rule.js",
|
|
52
|
+
"lib/",
|
|
53
|
+
"doc/",
|
|
46
54
|
"README.md",
|
|
47
55
|
"README_EN.md",
|
|
48
56
|
"package.json"
|
|
49
|
-
]
|
|
57
|
+
],
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"eslint": "^9.39.5",
|
|
60
|
+
"eslint-plugin-jsdoc": "^65.0.0"
|
|
61
|
+
}
|
|
50
62
|
}
|