mm_tpl 2.7.2 → 2.7.4

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 CHANGED
@@ -93,7 +93,7 @@ const tpl = new Tpl({
93
93
  plural_rules: {
94
94
  'en_us': function (count) { return count === 1 ? 0 : 1; }
95
95
  },
96
- // 复数规则,键为语言代码;mm_expand 的 Lang 已内置 zh_cn、en 规则,其他语言代码(如 en_us)在此登记,未登记时会回退到内置 en 规则
96
+ // 复数规则,键为语言代码;内置语言核心已带 zh_cn、en 规则,其他语言代码(如 en_us)在此登记,未登记时会回退到内置 en 规则
97
97
  rules: [nativeRule, artRule, htmlRule, jsRule, pyRule, pyRule2] // 模板规则
98
98
  });
99
99
 
@@ -176,8 +176,8 @@ const eachResult = tpl.render(`
176
176
  ### 5. 视图渲染
177
177
 
178
178
  ```javascript
179
- // 设置全局变量
180
- $.globalBag.siteName = "我的网站";
179
+ // 设置全局数据
180
+ Tpl.global_bag.siteName = "我的网站";
181
181
 
182
182
  // 设置视图变量
183
183
  tpl.set({
@@ -363,7 +363,7 @@ const tpl = new Tpl({
363
363
 
364
364
  #### 添加函数钩子
365
365
  ```javascript
366
- $.hook.addFunc('list', async function(viewBag, req, type) {
366
+ Tpl.global_hook.addFunc('list', async function(viewBag, req, type) {
367
367
  // 处理列表逻辑
368
368
  var query = req.query;
369
369
  if (query.user_id) {
@@ -375,14 +375,14 @@ $.hook.addFunc('list', async function(viewBag, req, type) {
375
375
 
376
376
  #### 添加动作钩子
377
377
  ```javascript
378
- $.hook.addAction('header', function(viewBag, param) {
378
+ Tpl.global_hook.addAction('header', function(viewBag, param) {
379
379
  return "<header>网站头部</header>";
380
380
  }, 1, "headerAction");
381
381
  ```
382
382
 
383
383
  #### 添加过滤器钩子
384
384
  ```javascript
385
- $.hook.addFilter('content', function(viewBag, content, param) {
385
+ Tpl.global_hook.addFilter('content', function(viewBag, content, param) {
386
386
  return content.replace("旧文本", "新文本");
387
387
  }, 1, "contentFilter");
388
388
  ```
@@ -407,12 +407,12 @@ const html = tpl.render(`
407
407
  #### 删除钩子
408
408
  ```javascript
409
409
  // 删除指定钩子
410
- $.hook.delFunc('list', 'listFunc');
411
- $.hook.delAction('header', 'headerAction');
412
- $.hook.delFilter('content', 'contentFilter');
410
+ Tpl.global_hook.delFunc('list', 'listFunc');
411
+ Tpl.global_hook.delAction('header', 'headerAction');
412
+ Tpl.global_hook.delFilter('content', 'contentFilter');
413
413
 
414
414
  // 清空所有钩子
415
- $.hook.clear();
415
+ Tpl.global_hook.clear();
416
416
  ```
417
417
 
418
418
  ### 8. 多用户风格与语言
@@ -519,7 +519,7 @@ tpl.tc('items', 5); // 英文为 "1 item" / "5 items"
519
519
  { "items": ["{count} item", "{count} items"] }
520
520
  ```
521
521
 
522
- 复数形式由语言的复数规则决定。语言代码统一用小写下划线(如 `zh_cn`、`en_us`),与语言包文件名一致,也便于浏览器端引用。`zh_cn`、`en` 是 mm_expand 的 Lang 内置的规则,可直接命中;其他语言代码(如 `en_us`)建议在配置项 `plural_rules` 中显式登记,`tc()` 才能按该语言取到正确的复数形式;未登记时会回退到内置的 `en` 规则。
522
+ 复数形式由语言的复数规则决定。语言代码统一用小写下划线(如 `zh_cn`、`en_us`),与语言包文件名一致,也便于浏览器端引用。`zh_cn`、`en` 是内置语言核心带的规则,可直接命中;其他语言代码(如 `en_us`)建议在配置项 `plural_rules` 中显式登记,`tc()` 才能按该语言取到正确的复数形式;未登记时会回退到内置的 `en` 规则。
523
523
 
524
524
  #### 语言包文件
525
525
 
@@ -738,13 +738,19 @@ const restored = tpl.escape(escaped, true);
738
738
  // 输出: <div>${content}</div>
739
739
  ```
740
740
 
741
- ### 5. 全局挂载
741
+ ### 5. 全局静态属性
742
+
743
+ 模块不挂载任何全局变量,跨实例共享的状态统一放在主类的静态属性上:
744
+
742
745
  ```javascript
743
- // 自动挂载到全局对象
744
- if (global.$ && !$.Tpl) {
745
- $.Tpl = Tpl;
746
- $.tpl = new Tpl();
747
- }
746
+ // 全局钩子单例,往里加的钩子对所有实例生效
747
+ Tpl.global_hook.addAction('header', function (m) { return '<header>头部</header>'; });
748
+
749
+ // 全局数据袋,渲染时注入模型的 globalBag 字段
750
+ Tpl.global_bag.siteName = '我的网站';
751
+
752
+ // 全局语言包字典,lang_dir 与 langs 都为空时实例按深拷贝副本兜底复用
753
+ Tpl.global_dict = { zh_cn: { name: '超级美眉' }, en: { name: 'MM' } };
748
754
  ```
749
755
 
750
756
  ### 6. 多用户风格与语言的使用方式
@@ -918,7 +924,7 @@ tpl3.getStore(); // 取到该存储对象
918
924
 
919
925
  ```javascript
920
926
  // 错误:钩子把请求数据写进 viewBag,同一实例的后续请求会读到
921
- $.hook.addFunc('login', async function (m, req) {
927
+ Tpl.global_hook.addFunc('login', async function (m, req) {
922
928
  m.user_id = req.user_id; // m 就是 this.viewBag
923
929
  });
924
930
 
@@ -985,7 +991,7 @@ tpl.render('${$md(\'**加粗**\')}'); // <b>加粗</b>
985
991
 
986
992
  #### 实例级钩子
987
993
 
988
- `$.hook` 是**全局单例**,往里加的钩子对所有实例生效;`tpl.hook` 是**实例级钩子**,只影响当前实例及其派生实例。渲染时两级都会执行,**全局先执行、实例级后执行**:动作钩子的输出全局在前,过滤器钩子的返回值由全局传给实例级继续处理。
994
+ `Tpl.global_hook` 是**全局单例**,往里加的钩子对所有实例生效;`tpl.hook` 是**实例级钩子**,只影响当前实例及其派生实例。渲染时两级都会执行,**全局先执行、实例级后执行**:动作钩子的输出全局在前,过滤器钩子的返回值由全局传给实例级继续处理。
989
995
 
990
996
  ```javascript
991
997
  tpl.hook.addAction('header', function (m) { return '<header>实例级</header>'; });
@@ -1058,13 +1064,13 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1058
1064
  - `lang` - 当前语言实例(`Lang`),语言包字典在实例间共享,当前语言各自独立
1059
1065
  - `store` - 用户偏好存储(`Memory` 或 `Sqlite`),通过 `user_id` 映射用户的风格与语言,派生实例共享同一份存储
1060
1066
  - `funcs` - 实例级模板函数表,由 `setFunc` 写入
1061
- - `hook` - 实例级钩子(`Hook`),只影响当前实例及其派生实例;与全局 `$.hook` 一起执行,全局先执行
1067
+ - `hook` - 实例级钩子(`Hook`),只影响当前实例及其派生实例;与全局 `Tpl.global_hook` 一起执行,全局先执行
1062
1068
  - `dir` - 应用根目录,即 `config.root`
1063
1069
  - `error` - 错误提示
1064
1070
 
1065
1071
  ### 配置项说明
1066
1072
 
1067
- - `root` - 应用根目录,默认 `$.run_path`,主题、语言包、自定义覆盖目录均相对它定位
1073
+ - `root` - 应用根目录,默认当前工作目录,主题、语言包、自定义覆盖目录均相对它定位
1068
1074
  - `theme_dir` - 主题目录,相对应用根目录,默认 `'./static/template/'`
1069
1075
  - `style_dir` - 风格目录,相对主题目录,默认 `'./style/'`
1070
1076
  - `component_dir` - 组件目录,相对主题目录(`static/diy/` 下取同名子目录),默认 `'./component/'`,存放可被功能页面复用的公共片段;为空则不启用组件,模板内的 `$component` 取不到文件
@@ -1079,8 +1085,8 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1079
1085
  - `default_lang` - 默认语言代码,默认 `'zh_cn'`
1080
1086
  - `defaults` - 应用配置文件 `config.js` 的简写,形如 `{theme, style, lang}`,创建实例时展开为 `default_theme` / `default_style` / `default_lang`;显式传入的 `default_*` 优先,`defaults` 不会残留在 `config` 中,因此 `fork()` 不会重复展开
1081
1087
  - `extname` - 模板与组件文件的扩展名,默认 `'.html'`,可自行设定为 `'.art'` 等其他后缀;`getFile`、`getComponentFile`、`view` 与 `component` 在入参省略扩展名时会自动补全,因此 `view('./article/list')` 与 `view('./article/list.html')` 等价
1082
- - `plural_rules` - 复数规则,键为语言代码,默认登记 `'en_us'`;mm_expand 的 Lang 已内置 `zh_cn`、`en` 规则,其他语言代码需在此登记,`tc()` 才能按该语言取到正确的复数形式
1083
- - `langs` - 内存语言包字典,形如 `{'zh_cn': {...}, 'en_us': {...}}`,优先级高于语言包文件;它与 `lang_dir` 都为空时复用全局 `$.lang.config`
1088
+ - `plural_rules` - 复数规则,键为语言代码,默认登记 `'en_us'`;内置语言核心已带 `zh_cn`、`en` 规则,其他语言代码需在此登记,`tc()` 才能按该语言取到正确的复数形式
1089
+ - `langs` - 内存语言包字典,形如 `{'zh_cn': {...}, 'en_us': {...}}`,优先级高于语言包文件;它与 `lang_dir` 都为空时复用全局 `Tpl.global_dict`
1084
1090
  - `user_store` - 用户偏好存储,默认 `null`(用进程内存的 `Memory`);传入实现 `getUser` / `setUser` / `delUser` / `hasUser` / `listUsers` / `clear` 六个同步方法的对象即可替换为文件、数据库或缓存服务,也可直接用内置的 `Memory` / `Sqlite`,派生实例复用同一存储
1085
1091
  - `cache_dims` - 缓存分层维度,默认 `['style', 'lang']`,可选 `theme`、`style`、`lang`;顺序即目录层级,只应包含**用户可选**的维度
1086
1092
  - `cache_type` - 缓存方式,默认 `'file'`(物理文件),可选 `'memory'`(进程内存)
@@ -1099,11 +1105,11 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1099
1105
  - `delFilter(name, alias)` - 删除过滤器钩子
1100
1106
  - `clear(name)` - 清空钩子
1101
1107
 
1102
- 以上方法在全局单例 `$.hook` 与实例级 `tpl.hook` 上用法一致。区别在于作用范围:`$.hook` 影响所有实例,`tpl.hook` 只影响当前实例及其派生实例;渲染时两级都会执行,全局先执行、实例级后执行。
1108
+ 以上方法在全局单例 `Tpl.global_hook` 与实例级 `tpl.hook` 上用法一致。区别在于作用范围:`Tpl.global_hook` 影响所有实例,`tpl.hook` 只影响当前实例及其派生实例;渲染时两级都会执行,全局先执行、实例级后执行。
1103
1109
 
1104
1110
  ## 注意事项
1105
1111
 
1106
- 1. **依赖要求**: 需要安装 `art-template` 和 `mm_expand` 依赖包
1112
+ 1. **依赖要求**: 运行时仅依赖 `art-template`,路径、语言、日志等能力已内置,无需其他外部模块与全局变量
1107
1113
  2. **目录权限**: 确保模板目录具有读写权限
1108
1114
  3. **缓存管理**: 缓存按 `cache_dims` 分层存放,清除时注意路径层级;`clearCache` 不传路径时只清除当前实例的缓存目录,清除全部需显式传 `'./'`。模板或语言包改动后缓存会自动失效,通常不需要手动清理
1109
1115
  4. **钩子优先级**: 数值越小优先级越高,执行顺序越靠前
@@ -1111,14 +1117,14 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1111
1117
  6. **文件查找与输出顺序**: 模板按**主题**三段查找:`static/diy/` → 当前主题 → 默认主题,按功能路径在主题目录下拼接(如 `static/template/{主题}/article/list.html`),**不按风格分层**,同名覆盖取优先级最高的文件。组件走独立的**组件目录链**,与模板链同构、每级多一个固定的 `component/` 前缀:`static/diy/component/` → 当前主题 `component/` → 默认主题 `component/`,组件名不相对调用方文件解析,当前主题缺组件时回退默认主题。样式按「主题 + 风格」五段查找:`static/diy/` → 当前主题+当前风格 → 当前主题+基础风格 → 默认主题+当前风格 → 默认主题+基础风格;只回退主题,不回退风格,其他风格目录的文件不会被继承,公共样式放基础风格目录(`base_style`,默认 `base`)。脚本只按主题分层:`static/diy/` → 当前主题 → 默认主题 → 应用级 `static/js/`。样式与脚本同名只保留优先级最高的一份,属「引用多个」类资源,`getCssFiles` / `getJsFiles` 等按优先级**升序**返回(低优先级在前、高优先级在后),因为 CSS 后加载者生效、JS 顺序决定执行;语言包相反,按优先级降序合并
1112
1118
  7. **字符转义**: 自动处理模板语法字符,避免冲突
1113
1119
  8. **多用户隔离**: 主题是部署级配置,站点内所有用户一致,不参与用户选择;风格和语言是用户级配置。渲染可用**传参方式**(单实例,`view` 的 `options` 传 `style` / `lang`,渲染后自动恢复)或**派生实例**(`fork`),见[多用户风格与语言的使用方式](#6-多用户风格与语言的使用方式)。**`viewBag` 是实例级共享的**:用户级数据必须通过 `model` 传递,模板写 `${user_id}`,不可写入 `viewBag`(模板写 `${viewBag.user_id}`);只有为每个用户各派生一个独立实例时 `viewBag` 才是该用户私有的。不要把用户级数据 `set` 进共用实例的 `viewBag`,也不要让多个用户共用同一实例再切换 `current_style` / `lang`,否则会互相污染
1114
- 9. **语言包共享**: 配置项 `langs` 中已存在的语言对象在实例间共享,设计上只用于"选择";若在实例上通过 `lang.set()` 写入自定义词条,可能污染其他用户,确需写入请在 `fork` 后自行深拷贝。语言包文件每次读取都会生成新对象,不受此影响;`lang_dir` 与 `langs` 都为空时兜底复用的全局 `$.lang.config` 也按深拷贝副本使用,实例上写入不会污染全局
1120
+ 9. **语言包共享**: 配置项 `langs` 中已存在的语言对象在实例间共享,设计上只用于"选择";若在实例上通过 `lang.set()` 写入自定义词条,可能污染其他用户,确需写入请在 `fork` 后自行深拷贝。语言包文件每次读取都会生成新对象,不受此影响;`lang_dir` 与 `langs` 都为空时兜底复用的全局 `Tpl.global_dict` 也按深拷贝副本使用,实例上写入不会污染全局
1115
1121
  10. **缓存数量**: 缓存文件数 ≤ (命名风格数 + 1) × 语言数 × 被缓存的模板数,**与用户数无关**;未选风格单独算一种组合但不占目录层级(`default_style` 也为空时才会出现),未被请求的组合不产生文件。`cache_dims` 只应包含**用户可选**的维度(默认风格和语言),多一个维度就是成倍增长;可选值为 `theme`、`style`、`lang`,`user_id` 已不再支持,传入会告警并忽略该维度
1116
1122
  11. **按用户缓存的正确性风险**: 缓存内容依赖用户数据时,用户数据在缓存有效期内变化会读到过期内容(如充值后仍显示旧余额)。这种页面本就不适合整页缓存,应改为前端异步加载;确实要分用户存放时,显式指定 `cache_filename` 区分,或用内存缓存 + 较小 `cache_max`
1117
1123
  12. **主题不参与缓存分层**: 主题为部署级配置,默认不在 `cache_dims` 中。运行时切换主题会复用旧主题的缓存,`setTheme` 会输出告警;切换后需 `clearCache()`,或把 `theme` 纳入维度
1118
1124
  13. **主题与风格校验**: `setTheme` / `setStyle` 默认校验目录存在性,不存在时保持原值并告警;目录尚未创建时可用 `force` 参数跳过校验。风格为空字符串表示不设置风格,查找与缓存按默认风格回退
1119
1125
  14. **风格与主题回退(仅样式)**: `setStyle` 只校验该风格目录是否存在于当前主题或默认主题下,目录缺失时保持原值并告警;直接给 `current_style` 赋值则不校验。样式查找时**只回退主题**(当前主题+当前风格 → 当前主题+基础风格 → 默认主题+当前风格 → 默认主题+基础风格),**不回退风格**,其他风格目录的文件不会被继承。公共样式放入基础风格目录(`base_style`,默认 `base`),各风格目录只放差异文件;若某风格需要不同于其他风格的公共样式,可在自己的风格目录内放同名文件覆盖基础风格。模板不按风格分层,换风格不改变命中的模板
1120
1126
  15. **模板取词**: 模板文件需通过 `$t('key')` 才能输出多语言内容,硬编码文案不会随语言切换;文案应放在语言包目录下的 JSON 语言包中(文件名即语言代码,如 `zh_cn.json`、`en_us.json`),模板文件只负责取词
1121
- 16. **钩子作用域**: `$.hook` 是全局单例,钩子对所有实例生效;`tpl.hook` 只作用于当前实例及其派生实例,渲染时两级都执行(全局先执行、实例级后执行)。模板函数优先级为「本次 `model` 同名字段 > 内置函数 > 已注册的函数」
1127
+ 16. **钩子作用域**: `Tpl.global_hook` 是全局单例,钩子对所有实例生效;`tpl.hook` 只作用于当前实例及其派生实例,渲染时两级都执行(全局先执行、实例级后执行)。模板函数优先级为「本次 `model` 同名字段 > 内置函数 > 已注册的函数」
1122
1128
  16. **语言缓存开销**: 未从 `cache_dims` 去掉 `lang` 时,同一份模板会按语言各存一份缓存。若模板本身不做多语言,移除 `lang` 维度可减少缓存文件数量
1123
1129
  17. **语言文件变更**: 语言包文件仅在创建实例时读取,已创建的实例不会自动刷新;新建实例或 `fork` 会按文件指纹自动判断是否需要重新读取。需让已创建的实例生效时调用 `reloadLangs()` 或 `loadLang(lang)`。`setTheme` 会自动重新加载,使主题级语言包生效
1124
1130
  18. **语言包缓存**: 语言包按「目录组 + 文件指纹」缓存在静态属性 `Tpl.lang_cache` 中,文件未变化时 `fork` 不会重复解析;缓存仅在内存中,进程重启后重新读取。指纹依赖文件修改时间,同一时间粒度内的改动可能识别不到,此时可用 `reloadLangs(true)` 强制刷新
@@ -1135,7 +1141,7 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1135
1141
 
1136
1142
  模块文件布局:
1137
1143
 
1138
- - `index.js` - 模块入口,定义对外主类 `Tpl`,并导出存储类 `Memory` / `Sqlite`
1144
+ - `index.js` - 模块入口,定义对外主类 `Tpl`,并导出 `Engine` / `Hook` 与存储类 `Memory` / `Sqlite`,不挂载任何全局变量
1139
1145
  - `lib/engine.js` - `Engine`,单个引擎实例:渲染、缓存、主题/风格/语言编排、派生实例(`fork`),以及 `getThemeDir` / `getStyleDir` / `getTemplateDirs` / `getComponentDirs` / `getComponentFile` / `getStyleDirs` / `getJsDirs` / `getCssFiles` / `getJsFiles`
1140
1146
  - `lib/template.js` - `Template`,模板文件查找:按功能路径在主题目录下查找(`static/diy/` → 当前主题 → 默认主题),同时负责组件的目录链查找(`getComponent`)
1141
1147
  - `lib/asset.js` - `Asset`,资源加载基类:按目录优先级收集、同名去重、按优先级升序输出
@@ -1144,8 +1150,11 @@ tpl.hook.addAction('header', function (m) { return '<header>实例级</header>';
1144
1150
  - `lib/lang.js` - `Lang`,语言包合并与缓存
1145
1151
  - `lib/store.js` - `Memory`,内存存储:`user_id` → `{style, lang}`
1146
1152
  - `lib/sqlite.js` - `Sqlite`,SQLite 存储:与 `Memory` 同契约,数据持久化到文件(依赖 `node:sqlite`)
1147
- - `lib/util.js` - `TplUtil`,纯辅助静态方法
1148
- - `hook.js`、`tpl_rule.js` - 钩子与模板语法规则
1153
+ - `lib/util.js` - `TplUtil`,纯辅助静态方法(深合并、深拷贝、md5、取中间文本等)
1154
+ - `lib/base.js` - `Base`,最小基类:静态配置与实例配置合并、日志输出
1155
+ - `lib/file.js` - `FileUtil`,内置路径与文件工具:全路径解析、目录/文件校验、文件读取
1156
+ - `lib/base_lang.js` - `BaseLang`,内置语言核心:取词、复数规则、日期/数字格式化
1157
+ - `lib/hook.js`、`lib/tpl_rule.js` - 钩子与模板语法规则
1149
1158
 
1150
1159
  ```bash
1151
1160
  # 运行测试
package/README_EN.md CHANGED
@@ -174,8 +174,8 @@ const eachResult = tpl.render(`
174
174
  ### 5. View Rendering
175
175
 
176
176
  ```javascript
177
- // Set global variables
178
- $.globalBag.siteName = "My Website";
177
+ // Set global data
178
+ Tpl.global_bag.siteName = "My Website";
179
179
 
180
180
  // Set view variables
181
181
  tpl.set({
@@ -361,7 +361,7 @@ In-memory entries live in `Tpl.cache_store` keyed by the full cache file path an
361
361
 
362
362
  #### Add Function Hook
363
363
  ```javascript
364
- $.hook.addFunc('list', async function(viewBag, req, type) {
364
+ Tpl.global_hook.addFunc('list', async function(viewBag, req, type) {
365
365
  // Process list logic
366
366
  var query = req.query;
367
367
  if (query.user_id) {
@@ -373,14 +373,14 @@ $.hook.addFunc('list', async function(viewBag, req, type) {
373
373
 
374
374
  #### Add Action Hook
375
375
  ```javascript
376
- $.hook.addAction('header', function(viewBag, param) {
376
+ Tpl.global_hook.addAction('header', function(viewBag, param) {
377
377
  return "<header>Website Header</header>";
378
378
  }, 1, "headerAction");
379
379
  ```
380
380
 
381
381
  #### Add Filter Hook
382
382
  ```javascript
383
- $.hook.addFilter('content', function(viewBag, content, param) {
383
+ Tpl.global_hook.addFilter('content', function(viewBag, content, param) {
384
384
  return content.replace("old text", "new text");
385
385
  }, 1, "contentFilter");
386
386
  ```
@@ -405,12 +405,12 @@ const html = tpl.render(`
405
405
  #### Delete Hooks
406
406
  ```javascript
407
407
  // Delete specific hook
408
- $.hook.delFunc('list', 'listFunc');
409
- $.hook.delAction('header', 'headerAction');
410
- $.hook.delFilter('content', 'contentFilter');
408
+ Tpl.global_hook.delFunc('list', 'listFunc');
409
+ Tpl.global_hook.delAction('header', 'headerAction');
410
+ Tpl.global_hook.delFilter('content', 'contentFilter');
411
411
 
412
412
  // Clear all hooks
413
- $.hook.clear();
413
+ Tpl.global_hook.clear();
414
414
  ```
415
415
 
416
416
  ### 8. Multi-user Styles and Languages
@@ -514,7 +514,7 @@ Plural entries are arrays in the language pack, `{count}` is the count placehold
514
514
  { "items": ["{count} item", "{count} items"] }
515
515
  ```
516
516
 
517
- The plural form is decided by the plural rule of the language, registered by language code in the `plural_rules` option. Language codes are lowercase with underscores; the `Lang` class of mm_expand already ships `zh_cn` and `en` rules, so those two work directly, while other codes such as `en_us` should be registered explicitly in `plural_rules`. When a code is not registered, `tc()` falls back to the built-in `en` rule. Codes match language-pack filenames and are easier to reference from the browser.
517
+ The plural form is decided by the plural rule of the language, registered by language code in the `plural_rules` option. Language codes are lowercase with underscores; the built-in language core already ships `zh_cn` and `en` rules, so those two work directly, while other codes such as `en_us` should be registered explicitly in `plural_rules`. When a code is not registered, `tc()` falls back to the built-in `en` rule. Codes match language-pack filenames and are easier to reference from the browser.
518
518
 
519
519
  #### Language Pack Files
520
520
 
@@ -744,13 +744,19 @@ const restored = tpl.escape(escaped, true);
744
744
  // Output: <div>${content}</div>
745
745
  ```
746
746
 
747
- ### 5. Global Mounting
747
+ ### 5. Global Static Properties
748
+
749
+ The module mounts no global variables; state shared across instances lives on static properties of the main class:
750
+
748
751
  ```javascript
749
- // Automatically mount to global object
750
- if (global.$ && !$.Tpl) {
751
- $.Tpl = Tpl;
752
- $.tpl = new Tpl();
753
- }
752
+ // Global hook singleton; hooks added here affect every instance
753
+ Tpl.global_hook.addAction('header', function (m) { return '<header>Header</header>'; });
754
+
755
+ // Global data bag, injected into the model's globalBag field at render time
756
+ Tpl.global_bag.siteName = 'My Website';
757
+
758
+ // Global language dictionary, used as a deep-copied fallback when both lang_dir and langs are empty
759
+ Tpl.global_dict = { zh_cn: { name: 'MM Girl' }, en: { name: 'MM' } };
754
760
  ```
755
761
 
756
762
  ### 6. Using Styles and Languages Across Users
@@ -928,7 +934,7 @@ Data written into `viewBag` is left behind for later requests using the same ins
928
934
 
929
935
  ```javascript
930
936
  // Wrong: the hook writes request data into viewBag, and later requests on the same instance read it
931
- $.hook.addFunc('login', async function (m, req) {
937
+ Tpl.global_hook.addFunc('login', async function (m, req) {
932
938
  m.user_id = req.user_id; // m is this.viewBag
933
939
  });
934
940
 
@@ -995,7 +1001,7 @@ Precedence: **a field of the same name in this call's `model` > built-in functio
995
1001
 
996
1002
  #### Instance-level Hooks
997
1003
 
998
- `$.hook` is a **global singleton**, and hooks added to it affect every instance; `tpl.hook` is an **instance-level hook** affecting only the current instance and the instances derived from it. Both levels run during rendering, **global first, instance-level second**: for action hooks the global output comes first, and for filter hooks the global return value is passed on to the instance level.
1004
+ `Tpl.global_hook` is a **global singleton**, and hooks added to it affect every instance; `tpl.hook` is an **instance-level hook** affecting only the current instance and the instances derived from it. Both levels run during rendering, **global first, instance-level second**: for action hooks the global output comes first, and for filter hooks the global return value is passed on to the instance level.
999
1005
 
1000
1006
  ```javascript
1001
1007
  tpl.hook.addAction('header', function (m) { return '<header>instance-level</header>'; });
@@ -1068,13 +1074,13 @@ tpl.hook.addAction('header', function (m) { return '<header>instance-level</head
1068
1074
  - `lang` - Current language instance (`Lang`); the language pack dictionary is shared, while the current language is independent
1069
1075
  - `store` - User preference store (`Memory` or `Sqlite`) mapping a `user_id` to the user's style and language; derived instances share the same store
1070
1076
  - `funcs` - Instance-level template function table, written by `setFunc`
1071
- - `hook` - Instance-level hooks (`Hook`) affecting only the current instance and the instances derived from it; they run together with the global `$.hook`, global first
1077
+ - `hook` - Instance-level hooks (`Hook`) affecting only the current instance and the instances derived from it; they run together with the global `Tpl.global_hook`, global first
1072
1078
  - `dir` - Application root directory, i.e. `config.root`
1073
1079
  - `error` - Error message
1074
1080
 
1075
1081
  ### Configuration Options
1076
1082
 
1077
- - `root` - Application root directory, defaults to `$.run_path`; themes, language packs and the override directory are resolved relative to it
1083
+ - `root` - Application root directory, defaults to the current working directory; themes, language packs and the override directory are resolved relative to it
1078
1084
  - `theme_dir` - Theme directory, relative to the application root, defaults to `'./static/template/'`
1079
1085
  - `style_dir` - Style directory, relative to the theme directory, defaults to `'./style/'`
1080
1086
  - `component_dir` - Component directory, relative to the theme directory (the sub-directory of the same name under `static/diy/`), defaults to `'./component/'`, holding shared partials reusable by feature pages; empty disables components, so `$component` in templates finds no file
@@ -1089,8 +1095,8 @@ tpl.hook.addAction('header', function (m) { return '<header>instance-level</head
1089
1095
  - `default_lang` - Default language code, defaults to `'zh_cn'`
1090
1096
  - `defaults` - Shorthand for the `config.js` file, shaped as `{theme, style, lang}`, expanded into `default_theme` / `default_style` / `default_lang` when the instance is created; an explicit `default_*` wins, and `defaults` is not kept in `config`, so `fork()` never expands it twice
1091
1097
  - `extname` - Extension of template and component files, defaults to `'.html'` and is configurable (e.g. `'.art'`); `getFile`, `getComponentFile`, `view` and `component` append it automatically when the argument omits the extension, so `view('./article/list')` and `view('./article/list.html')` are equivalent
1092
- - `plural_rules` - Plural rules keyed by language code, registering `'en_us'` by default; the `Lang` class of mm_expand already ships `zh_cn` and `en` rules, so other language codes are registered here
1093
- - `langs` - In-memory language pack dictionary, e.g. `{'zh_cn': {...}, 'en_us': {...}}`, takes precedence over language files; when both it and `lang_dir` are empty, the global `$.lang.config` is used
1098
+ - `plural_rules` - Plural rules keyed by language code, registering `'en_us'` by default; the built-in language core already ships `zh_cn` and `en` rules, so other language codes are registered here
1099
+ - `langs` - In-memory language pack dictionary, e.g. `{'zh_cn': {...}, 'en_us': {...}}`, takes precedence over language files; when both it and `lang_dir` are empty, the global `Tpl.global_dict` is used
1094
1100
  - `user_store` - User preference store, `null` by default (an in-process `Memory`); pass an object implementing the six synchronous methods `getUser` / `setUser` / `delUser` / `hasUser` / `listUsers` / `clear` to replace it with a file, a database or a cache service, or use the built-in `Memory` / `Sqlite` directly; derived instances reuse the same store
1095
1101
  - `cache_dims` - Cache layering dimensions, defaults to `['style', 'lang']`; values may be `theme`, `style`, `lang`. The order is the directory order, and it should only contain **user-selectable** dimensions
1096
1102
  - `cache_type` - Cache type, defaults to `'file'` (physical files); `'memory'` keeps the cache in process memory
@@ -1109,11 +1115,11 @@ tpl.hook.addAction('header', function (m) { return '<header>instance-level</head
1109
1115
  - `delFilter(name, alias)` - Delete filter hook
1110
1116
  - `clear(name)` - Clear hooks
1111
1117
 
1112
- The methods above behave identically on the global `$.hook` singleton and on the instance-level `tpl.hook`. They differ in scope: `$.hook` affects every instance, while `tpl.hook` affects only the current instance and the instances derived from it; both levels run during rendering, global first and instance-level second.
1118
+ The methods above behave identically on the global `Tpl.global_hook` singleton and on the instance-level `tpl.hook`. They differ in scope: `Tpl.global_hook` affects every instance, while `tpl.hook` affects only the current instance and the instances derived from it; both levels run during rendering, global first and instance-level second.
1113
1119
 
1114
1120
  ## Notes
1115
1121
 
1116
- 1. **Dependencies**: Requires `art-template` and `mm_expand` dependency packages
1122
+ 1. **Dependencies**: Only `art-template` at runtime; path, language and logging capabilities are built in, with no other external modules or global variables required
1117
1123
  2. **Directory Permissions**: Ensure template directories have read/write permissions
1118
1124
  3. **Cache Management**: The cache is layered by `cache_dims`, mind the directory levels when clearing; `clearCache` without a path clears only the cache directory of the current instance, pass `'./'` to clear everything. Template or language pack changes invalidate the cache automatically, so manual clearing is usually unnecessary
1119
1125
  4. **Hook Priority**: Lower numerical values indicate higher priority and earlier execution
@@ -1121,7 +1127,7 @@ The methods above behave identically on the global `$.hook` singleton and on the
1121
1127
  6. **File Search and Output Order**: Each kind of resource has its own directory chain. Templates use **three** levels, `static/diy/` → current theme → default theme, and are **not layered by style**, so a style never changes which template is used; the path is relative to the theme directory and may include feature directories such as `article/list.html`. Components use **three** levels of their own, isomorphic to the template chain with one extra fixed `component/` prefix on every level: `static/diy/component/` → current theme `component/` → default theme `component/`; a component name is not resolved relative to the calling file, and a component missing from the current theme falls back to the default theme. Styles use **five** levels: `static/diy/` → current theme + current style → current theme + base style → default theme + current style → default theme + base style. Scripts use **four** levels: `static/diy/` → current theme → default theme → application-level `static/js/`. For templates the highest-priority same-named file wins, while styles and scripts keep only the highest-priority copy per filename. Styles and scripts are "reference many" resources, so `getCssFiles` / `getJsFiles` and the like return them in **ascending** priority order (lower priority first, higher priority after), because a later-loaded CSS rule is the one that takes effect and the order of JS decides execution; language packs are the opposite, merged in descending priority order
1122
1128
  7. **Character Escaping**: Automatic handling of template syntax characters to avoid conflicts
1123
1129
  8. **Multi-user Isolation**: The theme is a deployment-level setting, uniform for every user on the site and not user-selectable; style and language are user-level. Rendering can use **passing options** (a single instance, with `style` / `lang` in the `view` options, restored afterwards) or **deriving instances** (`fork`); see [Using Styles and Languages Across Users](#6-using-styles-and-languages-across-users). **`viewBag` is shared at instance level**: user-level data must be passed through `model` (templates read `${user_id}`) and never written into `viewBag` (templates read `${viewBag.user_id}`); only when one separate instance is derived per user is `viewBag` private to that user. Do not `set` user-level data into a shared instance's `viewBag`, and do not let multiple users share one instance and switch `current_style` / `lang` on it, otherwise they will pollute each other
1124
- 9. **Shared Language Packs**: Language objects already present in the `langs` option are shared across instances and are meant for "picking" only; writing custom entries via `lang.set()` on an instance may pollute other users — deep-copy `langs` after `fork` if you really need to write. Language pack files are re-read into new objects, so they are not affected; the global `$.lang.config` used as a fallback when both `lang_dir` and `langs` are empty is also copied deeply, so writing on an instance never pollutes the global one
1130
+ 9. **Shared Language Packs**: Language objects already present in the `langs` option are shared across instances and are meant for "picking" only; writing custom entries via `lang.set()` on an instance may pollute other users — deep-copy `langs` after `fork` if you really need to write. Language pack files are re-read into new objects, so they are not affected; the global `Tpl.global_dict` used as a fallback when both `lang_dir` and `langs` are empty is also copied deeply, so writing on an instance never pollutes the global one
1125
1131
  10. **Cache Count**: Cache files ≤ (named styles + 1) × languages × cached templates, **independent of the number of users**; "no style selected" counts as a distinct combination but occupies no directory level (it only appears when `default_style` is empty too), and combinations that are never requested produce no file. `cache_dims` should only contain **user-selectable** dimensions (style and language by default), since every extra dimension multiplies the count; the accepted values are `theme`, `style` and `lang`, and `user_id` is no longer supported — passing it logs a warning and the dimension is ignored
1126
1132
  11. **Correctness Risk of Per-user Caching**: When cached content depends on user data, such content is served stale for as long as the cache lives (a user who tops up still sees the old balance). Such pages are not suitable for full-page caching; fetch the user-specific regions asynchronously. When per-user separation is genuinely required, pass an explicit `cache_filename`, or use the in-memory cache with a small `cache_max`
1127
1133
  12. **Theme Not in Cache Layering**: The theme is a deployment-level setting and is excluded from `cache_dims` by default. Switching the theme at runtime reuses the previous theme's cache and `setTheme` logs a warning; call `clearCache()` after switching, or include `theme` in the dimensions
@@ -1137,7 +1143,7 @@ The methods above behave identically on the global `$.hook` singleton and on the
1137
1143
  22. **In-memory and File Caches Are Separate**: The two modes read and write different locations, so switching `cache_type` leaves the old cache behind — call `clearCache()` as needed
1138
1144
  23. **Temporary Options of Passing-Render**: `options.style` and `options.lang` on `view` / `render` / `getCacheFile` apply to that call only and are restored afterwards without changing the instance state; `view` renders synchronously, so it is safe under concurrent requests. Keep them distinct from `setStyle` / `setLang`, which change the instance state persistently
1139
1145
  24. **User Preference Store**: `user_store` defaults to an in-process `Memory` and dies with the process; inject shared storage (the built-in `Sqlite`, or an object implementing the six synchronous methods `getUser` / `setUser` / `delUser` / `hasUser` / `listUsers` / `clear`) for multi-process or multi-machine deployments, otherwise each process sees only its own data. The store performs no authorisation, so the caller must ensure `user_id` is trustworthy
1140
- 25. **Hook Scope**: `$.hook` is a global singleton whose hooks affect every instance, while `tpl.hook` affects only the current instance and the instances derived from it; both levels run during rendering (global first, instance-level second). Template function precedence is "a same-named field in this call's `model` > built-in functions > registered functions"
1146
+ 25. **Hook Scope**: `Tpl.global_hook` is a global singleton whose hooks affect every instance, while `tpl.hook` affects only the current instance and the instances derived from it; both levels run during rendering (global first, instance-level second). Template function precedence is "a same-named field in this call's `model` > built-in functions > registered functions"
1141
1147
  26. **Object Literals Inside Expressions**: An object literal inside `${ }` is parsed in full, so `${@$component('bar', { id: 1 })}` and `${$t('welcome', { name: 'Tom' })}` can be written directly without assigning to a variable first. Only **non-nested** literals are supported: nested forms such as `{a:{b:1}}` are not recognised as an expression and are emitted as plain text (no error is thrown) — use the comment-style `<!--{@ }-->` delimiter or assign to a variable instead
1142
1148
  27. **Component Model and Inheritance**: The `$component` model is merged with the caller's data (`Object.assign`); the caller's data and template helpers such as `$view` / `$css` / `$t` are all passed down to the component, and fields in the passed model override the caller's
1143
1149
 
@@ -1145,7 +1151,7 @@ The methods above behave identically on the global `$.hook` singleton and on the
1145
1151
 
1146
1152
  Module layout:
1147
1153
 
1148
- - `index.js` - Module entry, defines the public main class `Tpl` and exports the store classes `Memory` / `Sqlite`
1154
+ - `index.js` - Module entry, defines the public main class `Tpl` and exports `Engine` / `Hook` plus the store classes `Memory` / `Sqlite`, mounting no global variables
1149
1155
  - `lib/engine.js` - `Engine`, a single engine instance: rendering, caching, the theme/style/language orchestration and instance derivation (`fork`), plus `getThemeDir` / `getStyleDir` / `getTemplateDirs` / `getComponentDirs` / `getComponentFile` / `getStyleDirs` / `getJsDirs` / `getCssFiles` / `getJsFiles`
1150
1156
  - `lib/template.js` - `Template`, template file lookup: resolves a template under the theme directory by feature path (`static/diy/` → current theme → default theme), and also resolves components along their own directory chain (`getComponent`)
1151
1157
  - `lib/asset.js` - `Asset`, the asset loading base class for styles and scripts: collecting by directory priority, de-duplicating by filename and emitting in ascending priority order
@@ -1154,8 +1160,11 @@ Module layout:
1154
1160
  - `lib/lang.js` - `Lang`, language pack merging and caching
1155
1161
  - `lib/store.js` - `Memory`, the in-memory store: `user_id` → `{style, lang}`
1156
1162
  - `lib/sqlite.js` - `Sqlite`, the SQLite store: same contract as `Memory`, persisting to a file (relies on `node:sqlite`)
1157
- - `lib/util.js` - `TplUtil`, pure static helper methods
1158
- - `hook.js`, `tpl_rule.js` - hooks and template syntax rules
1163
+ - `lib/util.js` - `TplUtil`, pure static helper methods (deep merge, deep clone, md5, text between markers, etc.)
1164
+ - `lib/base.js` - `Base`, minimal base class: merges static and instance config, logs output
1165
+ - `lib/file.js` - `FileUtil`, built-in path and file utilities: full-path resolution, directory/file checks, file reading
1166
+ - `lib/base_lang.js` - `BaseLang`, built-in language core: word lookup, plural rules, date/number formatting
1167
+ - `lib/hook.js`, `lib/tpl_rule.js` - hooks and template syntax rules
1159
1168
 
1160
1169
  ```bash
1161
1170
  # Run tests
package/index.js CHANGED
@@ -1,46 +1,28 @@
1
- const Hook = require('./lib/hook.js');
2
1
  const {
3
- Engine
4
- } = require('./lib/engine.js');
5
- const {
6
- Memory
7
- } = require('./lib/memory.js');
8
-
9
- /**
10
- * SQLite 存储
11
- * 依赖 Node 内置的 node:sqlite(Node 22.5 起提供),低版本加载失败时仅不导出,其余功能不受影响
12
- */
13
- let Sqlite = null;
14
-
15
- try {
16
- Sqlite = require('./lib/sqlite.js').Sqlite;
17
- } catch {
18
- Sqlite = null;
19
- }
20
-
21
- if (!$.globalBag) {
22
- $.globalBag = {};
23
- }
24
-
25
- if (!$.hook) {
26
- $.hook = new Hook();
27
- }
2
+ Tpl
3
+ } = require('./lib/tpl.js');
4
+ const Admin = require('mm_corejs').Admin;
28
5
 
29
6
  /**
30
- * 模板类
31
- * 对外主类与模块入口,在引擎实例基础上供业务侧扩展
32
- * 渲染、主题/风格/语言编排与派生实例见 lib/engine.js,默认配置项由 Engine 提供
33
- * @class
7
+ * 模板引擎实例(Admin 工厂)
8
+ * 支持 os.js 调用:require('mm_tpl').admin.setServer(os) -> admin.get('os', config)
34
9
  */
35
- class Tpl extends Engine { }
36
-
37
- if (global.$ && !$.Tpl) {
38
- $.Tpl = Tpl;
39
- $.tpl = new Tpl();
40
- }
10
+ let admin = new Admin();
11
+ admin.setClass(Tpl);
41
12
 
42
13
  module.exports = {
43
14
  Tpl,
44
- Memory,
45
- Sqlite
15
+ Hook: require('./lib/hook.js'),
16
+ Memory: require('./lib/memory.js'),
17
+ Sqlite: require('./lib/sqlite.js'),
18
+ admin,
19
+ tplAdmin(scope, config) {
20
+ if (!scope) {
21
+ scope = 'os';
22
+ }
23
+ return admin.get(scope, config);
24
+ },
25
+ getTpl() {
26
+ return admin.get('os');
27
+ }
46
28
  };
package/lib/asset.js CHANGED
@@ -1,3 +1,7 @@
1
+ const {
2
+ FileUtil
3
+ } = require('./file.js');
4
+
1
5
  /**
2
6
  * 资源加载基类
3
7
  * 样式与脚本同属「引用多个文件」类资源:按目录优先级收集,同名文件只保留优先级最高的一份
@@ -41,14 +45,14 @@ Asset.prototype.getDirs = function (theme, style) {
41
45
  * @returns {Array<string>} 资源文件路径列表,同目录内按文件名排序
42
46
  */
43
47
  Asset.prototype._matchDir = function (dir, keyword) {
44
- if (!dir || !dir.hasDir()) {
48
+ if (!dir || !FileUtil.hasDir(dir)) {
45
49
  return [];
46
50
  }
47
51
  // 排序保证同目录内的层叠与执行顺序稳定,不依赖文件系统的枚举顺序
48
- var files = $.file.get(dir, keyword).sort();
52
+ var files = FileUtil.listFiles(dir, keyword).sort();
49
53
  var list = [];
50
54
  for (var i = 0; i < files.length; i++) {
51
- if (files[i].extname() === this.extname) {
55
+ if (FileUtil.extname(files[i]) === this.extname) {
52
56
  list.push(files[i]);
53
57
  }
54
58
  }
@@ -72,7 +76,7 @@ Asset.prototype.matchFiles = function (pattern, theme, style) {
72
76
  for (var i = 0; i < dirs.length; i++) {
73
77
  var files = this._matchDir(dirs[i], keyword);
74
78
  for (var j = 0; j < files.length; j++) {
75
- var name = files[j].basename();
79
+ var name = FileUtil.basename(files[j]);
76
80
  // 高优先级目录已收录同名文件时跳过,实现同名覆盖
77
81
  if (map[name]) {
78
82
  continue;
package/lib/base.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 基础类
3
+ * 提供配置合并与日志输出的最小能力,不依赖任何外部模块
4
+ * @class
5
+ */
6
+ class Base {
7
+ /**
8
+ * 配置参数
9
+ * @type {Object}
10
+ */
11
+ static config = {};
12
+
13
+ /**
14
+ * 创建实例
15
+ * 实例配置为类静态配置的浅拷贝,再合并传入的配置
16
+ * @param {Object} config - 配置参数
17
+ */
18
+ constructor(config) {
19
+ /**
20
+ * 配置参数
21
+ * @type {Object}
22
+ */
23
+ this.config = { ...this.constructor.config };
24
+ this.setConfig(config);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 设置配置参数
30
+ * @param {Object} config - 配置参数
31
+ * @returns {Object} 当前实例的配置
32
+ */
33
+ Base.prototype.setConfig = function (config) {
34
+ if (config) {
35
+ if (typeof (config) !== 'object') {
36
+ throw new TypeError('配置参数必须是对象');
37
+ }
38
+ this._setConfig(config);
39
+ }
40
+ this._preset();
41
+ return this.config;
42
+ };
43
+
44
+ /**
45
+ * 获取当前配置
46
+ * @returns {Object} 配置对象
47
+ */
48
+ Base.prototype.getConfig = function () {
49
+ return this.config;
50
+ };
51
+
52
+ /**
53
+ * 设置配置参数 - 内部方法,子类可覆盖以实现自定义合并逻辑
54
+ * @param {Object} config - 配置参数
55
+ * @private
56
+ */
57
+ Base.prototype._setConfig = function (config) {
58
+ Object.assign(this.config, config);
59
+ };
60
+
61
+ /**
62
+ * 预设值 - 内部方法,子类可覆盖,在配置设置完成后执行
63
+ * @private
64
+ */
65
+ Base.prototype._preset = function () { };
66
+
67
+ /**
68
+ * 输出日志
69
+ * 使用配置项 logger 指定的日志对象,未配置时回退到 console
70
+ * @param {string} level - 日志级别,如 warn、error
71
+ * @param {string} message - 日志消息
72
+ * @param {...any} args - 附加参数
73
+ */
74
+ Base.prototype.log = function (level, message, ...args) {
75
+ var logger = this.config.logger || console;
76
+ var fn = logger[level] || logger.log;
77
+ if (typeof (fn) === 'function') {
78
+ fn.call(logger, message, ...args);
79
+ }
80
+ };
81
+
82
+ module.exports = {
83
+ Base
84
+ };