mm_tpl 2.7.1 → 2.7.3
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 +894 -52
- package/README_EN.md +905 -53
- 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 +28 -471
- package/lib/asset.js +133 -0
- package/lib/base.js +84 -0
- package/lib/base_lang.js +200 -0
- package/lib/css.js +34 -0
- package/lib/file.js +258 -0
- package/lib/hook.js +309 -0
- package/lib/js.js +34 -0
- package/lib/lang.js +188 -0
- package/lib/memory.js +101 -0
- package/lib/sqlite.js +174 -0
- package/lib/template.js +129 -0
- package/lib/tpl.js +1279 -0
- package/lib/tpl_rule.js +288 -0
- package/lib/util.js +196 -0
- package/package.json +18 -4
- package/hook.js +0 -302
- package/tpl_rule.js +0 -287
package/lib/sqlite.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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 = Sqlite;
|
package/lib/template.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const {
|
|
2
|
+
FileUtil
|
|
3
|
+
} = require('./file.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 模板文件加载器
|
|
7
|
+
* 模板按目录优先级覆盖:高优先级目录存在同名文件时覆盖低优先级目录的同名文件
|
|
8
|
+
* 模板目录链由引擎按「自定义目录、当前主题、默认主题」给出,主题目录下按功能划分,如 static/template/{主题}/article/list.html
|
|
9
|
+
* 组件目录链同构,只是每级多一个固定的组件子目录前缀,如 static/template/{主题}/component/bar.html
|
|
10
|
+
* @class
|
|
11
|
+
*/
|
|
12
|
+
class Template {
|
|
13
|
+
/**
|
|
14
|
+
* 创建模板文件加载器
|
|
15
|
+
* @param {Object} tpl - 所属模板实例
|
|
16
|
+
*/
|
|
17
|
+
constructor(tpl) {
|
|
18
|
+
/**
|
|
19
|
+
* 所属模板实例
|
|
20
|
+
*/
|
|
21
|
+
this.tpl = tpl;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检查主题目录是否存在
|
|
27
|
+
* @param {string} theme - 主题名称
|
|
28
|
+
* @returns {boolean} 主题目录是否存在
|
|
29
|
+
*/
|
|
30
|
+
Template.prototype.hasTheme = function (theme) {
|
|
31
|
+
if (!theme) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return FileUtil.hasDir(this.tpl.getThemeDir(theme));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 检查风格目录是否存在
|
|
39
|
+
* 当前主题或默认主题下存在该风格即视为有效
|
|
40
|
+
* @param {string} style - 风格名称
|
|
41
|
+
* @returns {boolean} 风格目录是否存在
|
|
42
|
+
*/
|
|
43
|
+
Template.prototype.hasStyle = function (style) {
|
|
44
|
+
if (!style) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
var tpl = this.tpl;
|
|
48
|
+
var cg = tpl.config;
|
|
49
|
+
var themes = [tpl.current_theme];
|
|
50
|
+
if (cg.default_theme && cg.default_theme !== tpl.current_theme) {
|
|
51
|
+
themes.push(cg.default_theme);
|
|
52
|
+
}
|
|
53
|
+
for (var i = 0; i < themes.length; i++) {
|
|
54
|
+
if (themes[i] && FileUtil.hasDir(tpl.getStyleDir(themes[i], style))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 补全文件扩展名
|
|
63
|
+
* 入参已带其他扩展名或已带模板扩展名时保持原样
|
|
64
|
+
* @param {string} file - 文件名
|
|
65
|
+
* @returns {string} 补全后的文件名
|
|
66
|
+
*/
|
|
67
|
+
Template.prototype._fixExt = function (file) {
|
|
68
|
+
var cg = this.tpl.config;
|
|
69
|
+
if (file.replace('./', '').indexOf('.') === -1 && !file.endsWith(cg.extname)) {
|
|
70
|
+
return file + cg.extname;
|
|
71
|
+
}
|
|
72
|
+
return file;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 按目录优先级搜索文件
|
|
77
|
+
* @param {string} file - 文件名
|
|
78
|
+
* @param {Array<string>} dirs - 目录列表,顺序即优先级,为空时视为未命中
|
|
79
|
+
* @returns {string} 命中的文件全路径;未命中时返回最后一个候选路径,其 FileUtil.hasFile 为 false
|
|
80
|
+
*/
|
|
81
|
+
Template.prototype._locate = function (file, dirs) {
|
|
82
|
+
if (!dirs.length) {
|
|
83
|
+
// 返回不存在的路径,交由调用方按 FileUtil.hasFile 判断
|
|
84
|
+
return FileUtil.fullname(file, this.tpl.dir) + '.not-exist';
|
|
85
|
+
}
|
|
86
|
+
var f = FileUtil.fullname(file, dirs[0]);
|
|
87
|
+
for (var i = 1; i < dirs.length && !FileUtil.hasFile(f); i++) {
|
|
88
|
+
f = FileUtil.fullname(file, dirs[i]);
|
|
89
|
+
}
|
|
90
|
+
return f;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取模板文件
|
|
95
|
+
* 按目录优先级从高到低搜索,返回第一个存在的文件
|
|
96
|
+
* options.dir 指定时只在该目录查找,用于主题体系外的模板
|
|
97
|
+
* @param {string} file - 模板文件路径
|
|
98
|
+
* @param {Object} options - 配置参数,dir 为指定的查找目录
|
|
99
|
+
* @returns {string} 模板文件路径
|
|
100
|
+
*/
|
|
101
|
+
Template.prototype.getFile = function (file, options) {
|
|
102
|
+
if (typeof (file) !== 'string') {
|
|
103
|
+
throw new Error('模板文件路径必须是字符串');
|
|
104
|
+
}
|
|
105
|
+
file = this._fixExt(file);
|
|
106
|
+
var dirs = (options && options.dir) ? [FileUtil.fullname(options.dir, this.tpl.dir)] : this.tpl.getTemplateDirs();
|
|
107
|
+
return this._locate(file, dirs);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取组件文件
|
|
112
|
+
* 组件名相对组件目录解析,省略扩展名时自动补全;名称指向组件目录内的路径,可含子目录,如 article/card
|
|
113
|
+
* 组件名不相对调用方所在文件解析,而是沿组件目录链查找
|
|
114
|
+
* @param {string} name - 组件名称,如 bar、article/card
|
|
115
|
+
* @param {Object} options - 配置参数,dir 为指定的查找目录
|
|
116
|
+
* @returns {string} 组件文件路径
|
|
117
|
+
*/
|
|
118
|
+
Template.prototype.getComponent = function (name, options) {
|
|
119
|
+
if (typeof (name) !== 'string') {
|
|
120
|
+
throw new Error('组件名称必须是字符串');
|
|
121
|
+
}
|
|
122
|
+
var file = this._fixExt(name.replace(/^\.\//, ''));
|
|
123
|
+
var dirs = (options && options.dir) ? [FileUtil.fullname(options.dir, this.tpl.dir)] : this.tpl.getComponentDirs();
|
|
124
|
+
return this._locate(file, dirs);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
Template
|
|
129
|
+
};
|