mm_tpl 2.7.4 → 2.8.0
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 +1082 -1083
- package/README_EN.md +1066 -1067
- package/lib/lang.js +7 -4
- package/package.json +3 -2
- package/lib/base_lang.js +0 -200
package/README_EN.md
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
# mm_tpl
|
|
2
|
-
|
|
3
|
-
Super Meimei Template Engine Module, based on art-template engine, provides powerful MVC pattern view rendering functionality with hook system, theme switching, and caching mechanism.
|
|
4
|
-
|
|
5
|
-
[中文文档](README.md) | [English Documentation](README_EN.md)
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
- [Features](#features)
|
|
10
|
-
- [Installation](#installation)
|
|
11
|
-
- [Quick Start](#quick-start)
|
|
12
|
-
- [Advanced Features](#advanced-features)
|
|
13
|
-
- [API Reference](#api-reference)
|
|
14
|
-
- [Notes](#notes)
|
|
15
|
-
- [Development](#development)
|
|
16
|
-
- [License](#license)
|
|
17
|
-
|
|
18
|
-
## Features
|
|
19
|
-
|
|
20
|
-
- 🚀 High-performance template engine based on art-template
|
|
21
|
-
- 🔧 Support for multiple template syntax rules
|
|
22
|
-
- 🎨 Deployment-level themes with user-level style and language switching; templates live under the theme directory organised by feature, styles are layered by style inside the theme
|
|
23
|
-
- 🧩 Components in a dedicated directory referenced by `$component`, keeping shared partials apart from feature pages, with theme fallback and diy override
|
|
24
|
-
- 📜 Application-level + theme-level scripts in two tiers, not layered by style, with application-level shared scripts running first
|
|
25
|
-
- 🌐 Multi-user support: each user picks its own style and language, and shares cache by that combination
|
|
26
|
-
- 🗂️ JSON language packs at application level and theme level, merged by directory priority and reloadable at runtime
|
|
27
|
-
- ⚡ Built-in caching with physical files or process memory, invalidated automatically when templates change
|
|
1
|
+
# mm_tpl
|
|
2
|
+
|
|
3
|
+
Super Meimei Template Engine Module, based on art-template engine, provides powerful MVC pattern view rendering functionality with hook system, theme switching, and caching mechanism.
|
|
4
|
+
|
|
5
|
+
[中文文档](README.md) | [English Documentation](README_EN.md)
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Advanced Features](#advanced-features)
|
|
13
|
+
- [API Reference](#api-reference)
|
|
14
|
+
- [Notes](#notes)
|
|
15
|
+
- [Development](#development)
|
|
16
|
+
- [License](#license)
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- 🚀 High-performance template engine based on art-template
|
|
21
|
+
- 🔧 Support for multiple template syntax rules
|
|
22
|
+
- 🎨 Deployment-level themes with user-level style and language switching; templates live under the theme directory organised by feature, styles are layered by style inside the theme
|
|
23
|
+
- 🧩 Components in a dedicated directory referenced by `$component`, keeping shared partials apart from feature pages, with theme fallback and diy override
|
|
24
|
+
- 📜 Application-level + theme-level scripts in two tiers, not layered by style, with application-level shared scripts running first
|
|
25
|
+
- 🌐 Multi-user support: each user picks its own style and language, and shares cache by that combination
|
|
26
|
+
- 🗂️ JSON language packs at application level and theme level, merged by directory priority and reloadable at runtime
|
|
27
|
+
- ⚡ Built-in caching with physical files or process memory, invalidated automatically when templates change
|
|
28
28
|
- 🔌 Powerful hook system with both global and instance-level hooks
|
|
29
29
|
- 🧩 Template function table: register with `setFunc` and call directly inside templates, inherited automatically by derived instances
|
|
30
|
-
- 📁 Flexible directory structure; the `static/diy/` directory overrides same-named templates, styles and scripts of themes
|
|
31
|
-
- 🛡️ Comprehensive error handling mechanism
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npm install mm_tpl
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
### 1. Install Dependencies
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npm install mm_tpl
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 2. Initialize Template Engine
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
const {Tpl} = require('mm_tpl');
|
|
51
|
-
|
|
52
|
-
// Directory layout under the application root (see doc/目录结构.md):
|
|
53
|
-
// app/shop/
|
|
30
|
+
- 📁 Flexible directory structure; the `static/diy/` directory overrides same-named templates, styles and scripts of themes
|
|
31
|
+
- 🛡️ Comprehensive error handling mechanism
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install mm_tpl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Install Dependencies
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install mm_tpl
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Initialize Template Engine
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const {Tpl} = require('mm_tpl');
|
|
51
|
+
|
|
52
|
+
// Directory layout under the application root (see doc/目录结构.md):
|
|
53
|
+
// app/shop/
|
|
54
54
|
// config.js # { defaults: { theme, style, lang } }
|
|
55
55
|
// static/ # static resource root: templates, styles, scripts, language packs and the override directory all live here
|
|
56
56
|
// diy/ # user override directory, highest priority, may hold templates, components and scripts
|
|
@@ -65,795 +65,795 @@ const {Tpl} = require('mm_tpl');
|
|
|
65
65
|
// js/ # theme-level scripts, overriding same-named application-level scripts
|
|
66
66
|
// lang/ # theme-level language packs, overriding the application level
|
|
67
67
|
// style/{style}/ # styles only, no templates; styles are layered by style inside the theme
|
|
68
|
-
// style/base/ # base style: common styles shared by every style
|
|
69
|
-
|
|
70
|
-
// Create template instance
|
|
71
|
-
const tpl = new Tpl({
|
|
72
|
-
root: process.cwd(), // application root directory
|
|
73
|
-
theme_dir: './static/template/', // theme directory, relative to the application root
|
|
74
|
-
style_dir: './style/', // style directory, relative to the theme directory
|
|
75
|
-
component_dir: './component/', // component directory, relative to the theme directory, holds shared partials; empty disables components
|
|
76
|
-
diy_dir: './static/diy/', // user override directory, relative to the application root, highest priority
|
|
77
|
-
lang_dir: './static/lang/', // application-level language pack directory, filename is the language code, empty to disable
|
|
78
|
-
theme_lang_dir: './lang/', // theme-level language pack directory, relative to the theme directory, overrides same keys
|
|
79
|
-
js_dir: './static/js/', // application-level script directory, relative to the application root, holds shared scripts
|
|
80
|
-
theme_js_dir: './js/', // theme-level script directory, relative to the theme directory; scripts are not layered by style
|
|
81
|
-
default_theme: 'default', // default theme, a deployment-level setting users do not pick
|
|
68
|
+
// style/base/ # base style: common styles shared by every style
|
|
69
|
+
|
|
70
|
+
// Create template instance
|
|
71
|
+
const tpl = new Tpl({
|
|
72
|
+
root: process.cwd(), // application root directory
|
|
73
|
+
theme_dir: './static/template/', // theme directory, relative to the application root
|
|
74
|
+
style_dir: './style/', // style directory, relative to the theme directory
|
|
75
|
+
component_dir: './component/', // component directory, relative to the theme directory, holds shared partials; empty disables components
|
|
76
|
+
diy_dir: './static/diy/', // user override directory, relative to the application root, highest priority
|
|
77
|
+
lang_dir: './static/lang/', // application-level language pack directory, filename is the language code, empty to disable
|
|
78
|
+
theme_lang_dir: './lang/', // theme-level language pack directory, relative to the theme directory, overrides same keys
|
|
79
|
+
js_dir: './static/js/', // application-level script directory, relative to the application root, holds shared scripts
|
|
80
|
+
theme_js_dir: './js/', // theme-level script directory, relative to the theme directory; scripts are not layered by style
|
|
81
|
+
default_theme: 'default', // default theme, a deployment-level setting users do not pick
|
|
82
82
|
default_style: 'light', // default style, a variant inside a theme, user-selectable
|
|
83
|
-
base_style: 'base', // base style directory holding styles shared by all styles; empty disables it
|
|
84
|
-
default_lang: 'zh_cn', // default language, user-selectable
|
|
85
|
-
langs: {}, // in-memory language packs, take precedence over files
|
|
86
|
-
cache_dims: ['style', 'lang'], // cache layering dimensions; only user-selectable ones, users share cache by this
|
|
87
|
-
cache_type: 'file', // cache type: file for physical files, memory for process memory
|
|
88
|
-
cache_max: 1000, // max entries of the in-memory cache
|
|
89
|
-
extname: ".html", // template file extension, configurable (e.g. ".art"); appended automatically when omitted
|
|
90
|
-
cache_root: './cache', // cache directory
|
|
91
|
-
cache_extname: '.cache.html', // cache file extension
|
|
92
|
-
plural_rules: { // plural rules keyed by language code
|
|
93
|
-
'en_us': function (count) { return count === 1 ? 0 : 1; }
|
|
94
|
-
},
|
|
95
|
-
rules: [nativeRule, artRule, htmlRule, jsRule, pyRule, pyRule2] // template rules
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// You can also read the application config file directly; defaults is expanded into default_theme / default_style / default_lang
|
|
99
|
-
const app_config = require('./app/shop/config.js');
|
|
100
|
-
const tpl_shop = new Tpl(Object.assign({ root: './app/shop/' }, app_config));
|
|
101
|
-
// config.js content: module.exports = { defaults: { theme: 'default', style: 'light', lang: 'zh_cn' } }
|
|
102
|
-
// An explicit default_* wins over defaults, and defaults is not kept in config, so fork() never expands it twice
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 3. Basic Template Rendering
|
|
106
|
-
|
|
107
|
-
```javascript
|
|
108
|
-
// Simple variable substitution
|
|
109
|
-
const result = tpl.render("<div>${message}</div>", {
|
|
110
|
-
message: "Hello World"
|
|
111
|
-
});
|
|
112
|
-
// Output: <div>Hello World</div>
|
|
113
|
-
|
|
114
|
-
// Compile template function
|
|
115
|
-
const compiled = tpl.compile("<div>${name} - ${age}</div>");
|
|
116
|
-
const result2 = compiled({ name: "John", age: 25 });
|
|
117
|
-
// Output: <div>John - 25</div>
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### 4. Loop Rendering
|
|
121
|
-
|
|
122
|
-
```javascript
|
|
123
|
-
// Array loop
|
|
124
|
-
const arrayResult = tpl.render(`
|
|
125
|
-
<ul>
|
|
126
|
-
${loop arr value idx}
|
|
127
|
-
<li>${idx}: ${value}</li>
|
|
128
|
-
${/loop}
|
|
129
|
-
</ul>
|
|
130
|
-
`, {
|
|
131
|
-
arr: [123, 234]
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Object loop
|
|
135
|
-
const objectResult = tpl.render(`
|
|
136
|
-
<dl>
|
|
137
|
-
${loop obj value key}
|
|
138
|
-
<dt>${key}</dt>
|
|
139
|
-
<dd>${value}</dd>
|
|
140
|
-
${/loop}
|
|
141
|
-
</dl>
|
|
142
|
-
`, {
|
|
143
|
-
obj: { name: "John", age: 25 }
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Traditional for loop
|
|
147
|
-
const forResult = tpl.render(`
|
|
148
|
-
<ul>
|
|
149
|
-
<!--[ for(var i = 0; i < arr.length; i++){ ]-->
|
|
150
|
-
<li>${arr[i]}</li>
|
|
151
|
-
<!--[ } ]-->
|
|
152
|
-
</ul>
|
|
153
|
-
`, {
|
|
154
|
-
arr: [123, 234]
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Each loop
|
|
158
|
-
const eachResult = tpl.render(`
|
|
159
|
-
<ul>
|
|
160
|
-
${each arr}
|
|
161
|
-
<li>${$value}</li>
|
|
162
|
-
${/each}
|
|
163
|
-
</ul>
|
|
164
|
-
`, {
|
|
165
|
-
arr: [123, 234]
|
|
166
|
-
});
|
|
167
|
-
// Output:
|
|
168
|
-
// <ul>
|
|
169
|
-
// <li>123</li>
|
|
170
|
-
// <li>234</li>
|
|
171
|
-
// </ul>
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 5. View Rendering
|
|
175
|
-
|
|
176
|
-
```javascript
|
|
177
|
-
// Set global data
|
|
178
|
-
Tpl.global_bag.siteName = "My Website";
|
|
179
|
-
|
|
180
|
-
// Set view variables
|
|
181
|
-
tpl.set({
|
|
182
|
-
title: "Homepage",
|
|
183
|
-
keywords: "keywords"
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Render view file; the path is relative to the theme directory, e.g. static/template/{theme}/index.html,
|
|
187
|
-
// and may include feature directories such as article/list.html
|
|
188
|
-
const html = tpl.view("./index.html", {
|
|
189
|
-
user: { name: "John" }
|
|
190
|
-
});
|
|
191
|
-
// Output: Rendering result <div>Hello World</div>
|
|
192
|
-
|
|
193
|
-
// Get view variable
|
|
194
|
-
const title = tpl.get("title");
|
|
195
|
-
|
|
196
|
-
// Delete view variable
|
|
197
|
-
tpl.del("keywords");
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
#### Rendering Components
|
|
201
|
-
|
|
202
|
-
Shared partials (headers, sidebars, ad slots and so on) reusable by several pages live in the theme's `component/` directory, and pages reference them with `$component` using just the component name, without any directory prefix or extension:
|
|
203
|
-
|
|
204
|
-
```html
|
|
205
|
-
<!-- A page references a component: the ./component/ prefix and the .html suffix are omitted -->
|
|
206
|
-
<div>${@$component('bar')}</div>
|
|
207
|
-
|
|
208
|
-
<!-- A component can reference other components, nesting is supported -->
|
|
209
|
-
<!-- static/template/default/component/bar.html -->
|
|
210
|
-
<div class="ad">${@$component('ad')}</div>
|
|
211
|
-
|
|
212
|
-
<!-- Passing data to a component: the component reads the field directly -->
|
|
213
|
-
${@$component('user-card', { user: item })}
|
|
214
|
-
|
|
215
|
-
<!-- A component inside a sub-directory -->
|
|
216
|
-
${@$component('article/card')}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
The server-side equivalent:
|
|
220
|
-
|
|
221
|
-
```javascript
|
|
222
|
-
// Render a component, returns the rendered string
|
|
223
|
-
tpl.component('bar', { message: 'hello' });
|
|
224
|
-
|
|
225
|
-
// Return the full path of the hit component file without rendering
|
|
226
|
-
tpl.getComponentFile('bar');
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
A component accepts a model, which is merged with the caller's data: the caller's data and template helpers such as `$view` / `$css` / `$t` are passed down to the component, and fields in the passed model override the caller's. The model may be an object literal or a variable, the two are equivalent:
|
|
230
|
-
|
|
231
|
-
```html
|
|
232
|
-
${@$component('user-card', { user: item })}
|
|
233
|
-
${@$component('user-card', card_data)}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
A component name is resolved relative to the component directories, **not relative to the file that calls it**, so the same name resolves to the same component everywhere. When a component is not found, an empty string is returned and `tpl.error` is set (`code: 10000`) instead of throwing. See [Advanced Features](#1-theme-style-and-directory-support) for the full directory chain.
|
|
237
|
-
|
|
238
|
-
### 6. Caching Mechanism
|
|
239
|
-
|
|
240
|
-
```javascript
|
|
241
|
-
// Use cache for view rendering: cache: true is enough, the cache filename is derived from the template path
|
|
242
|
-
const html = tpl.view("./index.html", {
|
|
243
|
-
data: someData
|
|
244
|
-
}, {
|
|
245
|
-
cache: true
|
|
246
|
-
});
|
|
247
|
-
// Cache file: cache/{style}/{lang}/index.cache.html (cache_dims defaults to ['style', 'lang'])
|
|
248
|
-
// e.g. cache/light/zh_cn/index.cache.html, cache/dark/en_us/index.cache.html
|
|
249
|
-
|
|
250
|
-
// You can also pass an explicit cache filename
|
|
251
|
-
tpl.view("./index.html", { data: someData }, {
|
|
252
|
-
cache: true,
|
|
253
|
-
cache_filename: "./index" // Same as ./index.cache.html
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// cache: false only writes the cache without reading it, useful for a forced refresh
|
|
257
|
-
tpl.view("./index.html", { data: someData }, {
|
|
258
|
-
cache: false,
|
|
259
|
-
cache_filename: "./index"
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Clear cache: without arguments only the cache directory of the current instance is cleared
|
|
263
|
-
tpl.clearCache();
|
|
264
|
-
// Clear a specific directory, relative to cache_root
|
|
265
|
-
tpl.clearCache('./game');
|
|
266
|
-
// Clear all cache
|
|
267
|
-
tpl.clearCache('./');
|
|
268
|
-
|
|
269
|
-
// Use getCacheFile to inspect the cache path of the current instance; a string is accepted
|
|
270
|
-
tpl.getCacheFile('./index');
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### Cache Count Is Driven by cache_dims
|
|
274
|
-
|
|
275
|
-
`cache_dims` should only contain the dimensions **users can choose**. The division of responsibility in this module is:
|
|
276
|
-
|
|
277
|
-
| Dimension | Level | User-selectable | In cache layering |
|
|
278
|
-
| --- | --- | --- | --- |
|
|
279
|
-
| Theme `theme` | Deployment | No | No, uniform across the site |
|
|
280
|
-
| Style `style` | User | Yes | Yes |
|
|
281
|
-
| Language `lang` | User | Yes | Yes |
|
|
282
|
-
|
|
283
|
-
The default is therefore `['style', 'lang']`: users are grouped by "style + language", and users in the same combination share the same cache.
|
|
284
|
-
|
|
285
|
-
The upper bound on the number of cache files is:
|
|
286
|
-
|
|
287
|
-
```
|
|
288
|
-
cache files ≤ (named styles + 1) × languages × cached templates
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
The `+ 1` stands for **no style selected** — it is a distinct combination, but the empty string does not occupy a directory level, so its cache sits directly under the language directory (that combination only appears when `default_style` is empty too):
|
|
292
|
-
|
|
293
|
-
```
|
|
294
|
-
cache/dark/zh_cn/test.cache.html ← dark style selected, one extra style level
|
|
295
|
-
cache/zh_cn/test.cache.html ← no style, lang sits directly under the root
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
This is an **upper bound, not a fixed count**: only combinations and templates that are actually rendered produce a cache file, and a combination that is never requested has no file at all. With 2 named styles (dark, light, plus no style = 3 combinations), 2 languages and 20 templates:
|
|
299
|
-
|
|
300
|
-
| `cache_dims` | Levels | Files (upper bound) |
|
|
301
|
-
| --- | --- | --- |
|
|
302
|
-
| `['style', 'lang']` (default) | 3 combinations × 2 languages × 20 templates | 120 |
|
|
303
|
-
| `['lang']` | no style dimension, 2 languages × 20 templates | 40 |
|
|
304
|
-
| `[]` | no layering, 20 templates | 20 |
|
|
305
|
-
|
|
306
|
-
The dimensions only take **finite enumerations** such as style and language; a user merely falls into one of those combinations, so this count is **completely independent of the number of users**. With 5,000 users, 1 named style (dark, plus no style = 2 combinations), 2 languages and 2 templates, even when all 4 combinations are used only 8 cache files are produced:
|
|
307
|
-
|
|
308
|
-
```
|
|
309
|
-
cache/zh_cn/test.cache.html ← no style + Chinese
|
|
310
|
-
cache/en_us/test.cache.html ← no style + English
|
|
311
|
-
cache/dark/zh_cn/test.cache.html ← dark style + Chinese
|
|
312
|
-
cache/dark/en_us/test.cache.html ← dark style + English
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
The module provides no per-user dimension: each entry would serve exactly one user and only hit when "the same user revisits the same page while the data stays unchanged", yet user data (balance, orders, messages) usually changes on every request, so the hit rate approaches zero and stale content may be served. **Pages that contain user data are not suitable for full-page caching**; fetch those regions asynchronously, or use the in-memory cache described below.
|
|
316
|
-
|
|
317
|
-
#### Theme Does Not Participate in Cache Layering
|
|
318
|
-
|
|
319
|
-
A theme is a deployment-level setting, uniform for every user on the site and not switched at runtime, so `theme` is not part of `cache_dims` by default. This comes with one constraint: **switching the theme at runtime reuses the previous theme's cache**. The module logs a warning from `setTheme`; when a switch really is needed, clear the cache or include `theme` in the dimensions:
|
|
320
|
-
|
|
321
|
-
```javascript
|
|
322
|
-
// Option 1: clear the cache after switching the theme
|
|
323
|
-
tpl.setTheme('default');
|
|
324
|
-
tpl.clearCache('./');
|
|
325
|
-
|
|
326
|
-
// Option 2: include the theme when multiple themes coexist long term
|
|
327
|
-
const tpl_multi = new Tpl({
|
|
328
|
-
default_theme: 'game',
|
|
329
|
-
cache_dims: ['theme', 'style', 'lang']
|
|
330
|
-
});
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
#### Automatic Cache Invalidation
|
|
334
|
-
|
|
335
|
-
After a template file or a language pack file is modified, the old cache is invalidated and rebuilt on the next render — **no manual clearing needed**. The check compares file modification times, not file contents:
|
|
336
|
-
|
|
337
|
-
```javascript
|
|
338
|
-
// Edit a template during development and it takes effect on refresh, no clearCache needed
|
|
339
|
-
// Editing static/lang/zh_cn.json rebuilds the Chinese cache the same way
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
Language pack changes are only checked when the cache is layered by language (`cache_dims` contains `lang`); otherwise only the template file is checked.
|
|
343
|
-
|
|
344
|
-
#### In-memory Cache
|
|
345
|
-
|
|
346
|
-
When you want the cache to stay off disk and live only inside the process, use the in-memory cache:
|
|
347
|
-
|
|
348
|
-
```javascript
|
|
349
|
-
const tpl = new Tpl({
|
|
350
|
-
default_theme: 'game',
|
|
351
|
-
cache_type: 'memory', // file (default) for physical files, memory for process memory
|
|
352
|
-
cache_max: 1000 // Max entries in memory; the earliest written entry is evicted beyond it, 0 means unlimited
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
In-memory entries live in `Tpl.cache_store` keyed by the full cache file path and use least-recently-used eviction: a hit moves the entry to the tail, and the earliest written entry is evicted beyond `cache_max`. The cache is cleared on process restart, which means each process keeps its own copy in a multi-instance deployment.
|
|
357
|
-
|
|
358
|
-
`clearCache` clears both physical files and in-memory entries, and its scope is likewise limited by `cache_dims`.
|
|
359
|
-
|
|
360
|
-
### 7. Hook System
|
|
361
|
-
|
|
362
|
-
#### Add Function Hook
|
|
363
|
-
```javascript
|
|
364
|
-
Tpl.global_hook.addFunc('list', async function(viewBag, req, type) {
|
|
365
|
-
// Process list logic
|
|
366
|
-
var query = req.query;
|
|
367
|
-
if (query.user_id) {
|
|
368
|
-
viewBag.user_id = query.user_id;
|
|
369
|
-
}
|
|
370
|
-
return someData;
|
|
371
|
-
}, 100, 'listFunc'); // sort: priority, alias: alias
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
#### Add Action Hook
|
|
375
|
-
```javascript
|
|
376
|
-
Tpl.global_hook.addAction('header', function(viewBag, param) {
|
|
377
|
-
return "<header>Website Header</header>";
|
|
378
|
-
}, 1, "headerAction");
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### Add Filter Hook
|
|
382
|
-
```javascript
|
|
383
|
-
Tpl.global_hook.addFilter('content', function(viewBag, content, param) {
|
|
384
|
-
return content.replace("old text", "new text");
|
|
385
|
-
}, 1, "contentFilter");
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
#### Execute Hooks
|
|
389
|
-
```javascript
|
|
390
|
-
// Execute function hook
|
|
391
|
-
const result = await tpl.runFunc('list', { query: { page: 1 } }, 'article');
|
|
392
|
-
|
|
393
|
-
// Execute action hook
|
|
394
|
-
const actionResult = tpl.runHook('header', { param: 'value' });
|
|
395
|
-
|
|
396
|
-
// Use hooks in template
|
|
397
|
-
const html = tpl.render(`
|
|
398
|
-
<div>
|
|
399
|
-
${hookAction('header')}
|
|
400
|
-
${hookFilter('content', 'original content')}
|
|
401
|
-
</div>
|
|
402
|
-
`, data);
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
#### Delete Hooks
|
|
406
|
-
```javascript
|
|
407
|
-
// Delete specific hook
|
|
408
|
-
Tpl.global_hook.delFunc('list', 'listFunc');
|
|
409
|
-
Tpl.global_hook.delAction('header', 'headerAction');
|
|
410
|
-
Tpl.global_hook.delFilter('content', 'contentFilter');
|
|
411
|
-
|
|
412
|
-
// Clear all hooks
|
|
413
|
-
Tpl.global_hook.clear();
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### 8. Multi-user Styles and Languages
|
|
417
|
-
|
|
418
|
-
The module works only on the parameters it receives and does not care where they come from. Configuration has two levels:
|
|
419
|
-
|
|
420
|
-
- **Deployment-level**: theme, template directory, cache directory, language pack directory and so on — uniform across the site and held by the master instance
|
|
421
|
-
- **User-level**: style and language, chosen by each user
|
|
422
|
-
|
|
423
|
-
There are two ways to use them: **passing options** (a single instance, with `style` / `lang` passed in the `view` options, no derived instance needed) and **deriving instances** (`fork`). The former is recommended; the following uses `fork` so each option's effect is easy to see. See [Using Styles and Languages Across Users](#6-using-styles-and-languages-across-users) for the trade-offs.
|
|
424
|
-
|
|
425
|
-
```javascript
|
|
426
|
-
// Master instance: holds the deployment-level configuration
|
|
427
|
-
const tpl = new Tpl({
|
|
428
|
-
root: './app/shop/', // application root directory
|
|
429
|
-
default_theme: 'game', // theme is fixed at deployment level, users do not pick it
|
|
430
|
-
default_style: '', // default style when the user picks none
|
|
431
|
-
default_lang: 'zh_cn', // default language when the user picks none
|
|
432
|
-
lang_dir: './static/lang/', // application-level language pack directory, filename is the language code
|
|
433
|
-
langs: {}, // optional in-memory language packs, take precedence over files
|
|
434
|
-
// cache is layered by the user-selectable dimensions: style + language
|
|
435
|
-
cache_dims: ['style', 'lang']
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// User A: no style + Chinese
|
|
439
|
-
const tpl_a = tpl.fork();
|
|
440
|
-
// User B: dark style + English
|
|
441
|
-
const tpl_b = tpl.fork({ style: 'dark', lang: 'en_us' });
|
|
442
|
-
|
|
443
|
-
tpl_a.getTheme(); // "game", inherited from the master instance
|
|
444
|
-
tpl_a.getStyle(); // ""
|
|
445
|
-
tpl_a.getLang(); // "zh_cn"
|
|
446
|
-
|
|
447
|
-
tpl_b.getTheme(); // "game"
|
|
448
|
-
tpl_b.getStyle(); // "dark"
|
|
449
|
-
tpl_b.getLang(); // "en_us"
|
|
450
|
-
|
|
451
|
-
// Render separately; the language and the cache differ per configuration,
|
|
452
|
-
// while template lookup stays identical because templates are not layered by style
|
|
453
|
-
tpl_a.view('./index.html', { title: '首页' });
|
|
454
|
-
tpl_b.view('./index.html', { title: 'Home' });
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
Template directories are resolved by priority — `static/diy/` → current theme → default theme — so both user A and user B resolve to `static/template/game/index.html`; the style takes no part in template lookup (see [Advanced Features](#1-theme-style-and-directory-support) for the full order). With `default_theme: 'game'` above, the current theme is also the site default theme, so the chain has just the first two levels; under another current theme the default theme is the third:
|
|
458
|
-
|
|
459
|
-
```
|
|
460
|
-
static/diy/index.html ← user override directory, highest priority, shared by all instances
|
|
461
|
-
static/template/game/index.html ← current theme == default theme: user A and user B hit this very same file
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
Only the theme falls back, never the style. What the style changes is the stylesheet list (and the cache path, since `cache_dims` contains `style`), not the template:
|
|
465
|
-
|
|
466
|
-
```
|
|
467
|
-
user A (light style): static/template/game/style/light/light.css + the shared static/template/game/style/base/common.css
|
|
468
|
-
user B (dark style): static/template/game/style/dark/dark.css + the shared static/template/game/style/base/common.css
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
Styles are layered by style and collect every css along their own chain, keeping only the highest-priority copy per filename, so dark and light each read their own directory while their shared styles live in the base style directory instead of being duplicated.
|
|
472
|
-
|
|
473
|
-
`$t` is bound to the language of the current instance and can be used in both rendered strings and template files:
|
|
474
|
-
|
|
475
|
-
```html
|
|
476
|
-
<!-- Rendered string -->
|
|
477
|
-
<p>${$t('hello', 'User')}</p>
|
|
478
|
-
<!-- User A (zh_cn) output: <p>你好,User!</p> -->
|
|
479
|
-
<!-- User B (en_us) output: <p>Hello, User!</p> -->
|
|
480
|
-
|
|
481
|
-
<!-- Named interpolation: write {name} in the entry and pass an object -->
|
|
482
|
-
<p>${$t('welcome', { name: user.name })}</p>
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
Templates can also use `$view` for sub-templates, `$component` for components, `$css` for stylesheets and `$js` for scripts; all of them resolve through their own directory priority chain. The `$css` / `$js` name supports `*` wildcards and returns an array when one is used:
|
|
486
|
-
|
|
487
|
-
```html
|
|
488
|
-
<div>${@$component('bar')}</div>
|
|
489
|
-
<link rel="stylesheet" href="${$css('common.css')}">
|
|
490
|
-
${each $css('*.css') value}
|
|
491
|
-
<link rel="stylesheet" href="${value}">
|
|
492
|
-
${/each}
|
|
493
|
-
|
|
494
|
-
<!-- every script; app* or *promo* narrows by name, and the order is the execution order -->
|
|
495
|
-
${each $js('*.js') value}
|
|
496
|
-
<script src="${value}"></script>
|
|
497
|
-
${/each}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
In server-side code use `t` / `tc`, which are equivalent to `$t` in templates:
|
|
501
|
-
|
|
502
|
-
```javascript
|
|
503
|
-
tpl.t('hello', 'User'); // "你好,User!", replaces {0}, {1} ... placeholders in order
|
|
504
|
-
tpl.t('welcome', { name: '张三' }); // "欢迎,张三!", an object argument fills {name} placeholders
|
|
505
|
-
tpl.t('bar.nav'); // "游戏导航", supports a.b style nested keys
|
|
506
|
-
tpl.tc('items', 1); // Plural lookup, picks the form by language rule: "共 1 项" in Chinese
|
|
507
|
-
tpl.tc('items', 5); // "1 item" / "5 items" in English
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Plural entries are arrays in the language pack, `{count}` is the count placeholder:
|
|
511
|
-
|
|
512
|
-
```json
|
|
513
|
-
// static/lang/en_us.json
|
|
514
|
-
{ "items": ["{count} item", "{count} items"] }
|
|
515
|
-
```
|
|
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 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
|
-
|
|
519
|
-
#### Language Pack Files
|
|
520
|
-
|
|
521
|
-
Keep copy in JSON language packs and let template files call `$t` only; do not hardcode copy in templates. The filename is the language code:
|
|
522
|
-
|
|
523
|
-
```
|
|
83
|
+
base_style: 'base', // base style directory holding styles shared by all styles; empty disables it
|
|
84
|
+
default_lang: 'zh_cn', // default language, user-selectable
|
|
85
|
+
langs: {}, // in-memory language packs, take precedence over files
|
|
86
|
+
cache_dims: ['style', 'lang'], // cache layering dimensions; only user-selectable ones, users share cache by this
|
|
87
|
+
cache_type: 'file', // cache type: file for physical files, memory for process memory
|
|
88
|
+
cache_max: 1000, // max entries of the in-memory cache
|
|
89
|
+
extname: ".html", // template file extension, configurable (e.g. ".art"); appended automatically when omitted
|
|
90
|
+
cache_root: './cache', // cache directory
|
|
91
|
+
cache_extname: '.cache.html', // cache file extension
|
|
92
|
+
plural_rules: { // plural rules keyed by language code
|
|
93
|
+
'en_us': function (count) { return count === 1 ? 0 : 1; }
|
|
94
|
+
},
|
|
95
|
+
rules: [nativeRule, artRule, htmlRule, jsRule, pyRule, pyRule2] // template rules
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// You can also read the application config file directly; defaults is expanded into default_theme / default_style / default_lang
|
|
99
|
+
const app_config = require('./app/shop/config.js');
|
|
100
|
+
const tpl_shop = new Tpl(Object.assign({ root: './app/shop/' }, app_config));
|
|
101
|
+
// config.js content: module.exports = { defaults: { theme: 'default', style: 'light', lang: 'zh_cn' } }
|
|
102
|
+
// An explicit default_* wins over defaults, and defaults is not kept in config, so fork() never expands it twice
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. Basic Template Rendering
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// Simple variable substitution
|
|
109
|
+
const result = tpl.render("<div>${message}</div>", {
|
|
110
|
+
message: "Hello World"
|
|
111
|
+
});
|
|
112
|
+
// Output: <div>Hello World</div>
|
|
113
|
+
|
|
114
|
+
// Compile template function
|
|
115
|
+
const compiled = tpl.compile("<div>${name} - ${age}</div>");
|
|
116
|
+
const result2 = compiled({ name: "John", age: 25 });
|
|
117
|
+
// Output: <div>John - 25</div>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Loop Rendering
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// Array loop
|
|
124
|
+
const arrayResult = tpl.render(`
|
|
125
|
+
<ul>
|
|
126
|
+
${loop arr value idx}
|
|
127
|
+
<li>${idx}: ${value}</li>
|
|
128
|
+
${/loop}
|
|
129
|
+
</ul>
|
|
130
|
+
`, {
|
|
131
|
+
arr: [123, 234]
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Object loop
|
|
135
|
+
const objectResult = tpl.render(`
|
|
136
|
+
<dl>
|
|
137
|
+
${loop obj value key}
|
|
138
|
+
<dt>${key}</dt>
|
|
139
|
+
<dd>${value}</dd>
|
|
140
|
+
${/loop}
|
|
141
|
+
</dl>
|
|
142
|
+
`, {
|
|
143
|
+
obj: { name: "John", age: 25 }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Traditional for loop
|
|
147
|
+
const forResult = tpl.render(`
|
|
148
|
+
<ul>
|
|
149
|
+
<!--[ for(var i = 0; i < arr.length; i++){ ]-->
|
|
150
|
+
<li>${arr[i]}</li>
|
|
151
|
+
<!--[ } ]-->
|
|
152
|
+
</ul>
|
|
153
|
+
`, {
|
|
154
|
+
arr: [123, 234]
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Each loop
|
|
158
|
+
const eachResult = tpl.render(`
|
|
159
|
+
<ul>
|
|
160
|
+
${each arr}
|
|
161
|
+
<li>${$value}</li>
|
|
162
|
+
${/each}
|
|
163
|
+
</ul>
|
|
164
|
+
`, {
|
|
165
|
+
arr: [123, 234]
|
|
166
|
+
});
|
|
167
|
+
// Output:
|
|
168
|
+
// <ul>
|
|
169
|
+
// <li>123</li>
|
|
170
|
+
// <li>234</li>
|
|
171
|
+
// </ul>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 5. View Rendering
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Set global data
|
|
178
|
+
Tpl.global_bag.siteName = "My Website";
|
|
179
|
+
|
|
180
|
+
// Set view variables
|
|
181
|
+
tpl.set({
|
|
182
|
+
title: "Homepage",
|
|
183
|
+
keywords: "keywords"
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Render view file; the path is relative to the theme directory, e.g. static/template/{theme}/index.html,
|
|
187
|
+
// and may include feature directories such as article/list.html
|
|
188
|
+
const html = tpl.view("./index.html", {
|
|
189
|
+
user: { name: "John" }
|
|
190
|
+
});
|
|
191
|
+
// Output: Rendering result <div>Hello World</div>
|
|
192
|
+
|
|
193
|
+
// Get view variable
|
|
194
|
+
const title = tpl.get("title");
|
|
195
|
+
|
|
196
|
+
// Delete view variable
|
|
197
|
+
tpl.del("keywords");
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Rendering Components
|
|
201
|
+
|
|
202
|
+
Shared partials (headers, sidebars, ad slots and so on) reusable by several pages live in the theme's `component/` directory, and pages reference them with `$component` using just the component name, without any directory prefix or extension:
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<!-- A page references a component: the ./component/ prefix and the .html suffix are omitted -->
|
|
206
|
+
<div>${@$component('bar')}</div>
|
|
207
|
+
|
|
208
|
+
<!-- A component can reference other components, nesting is supported -->
|
|
209
|
+
<!-- static/template/default/component/bar.html -->
|
|
210
|
+
<div class="ad">${@$component('ad')}</div>
|
|
211
|
+
|
|
212
|
+
<!-- Passing data to a component: the component reads the field directly -->
|
|
213
|
+
${@$component('user-card', { user: item })}
|
|
214
|
+
|
|
215
|
+
<!-- A component inside a sub-directory -->
|
|
216
|
+
${@$component('article/card')}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The server-side equivalent:
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
// Render a component, returns the rendered string
|
|
223
|
+
tpl.component('bar', { message: 'hello' });
|
|
224
|
+
|
|
225
|
+
// Return the full path of the hit component file without rendering
|
|
226
|
+
tpl.getComponentFile('bar');
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A component accepts a model, which is merged with the caller's data: the caller's data and template helpers such as `$view` / `$css` / `$t` are passed down to the component, and fields in the passed model override the caller's. The model may be an object literal or a variable, the two are equivalent:
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
${@$component('user-card', { user: item })}
|
|
233
|
+
${@$component('user-card', card_data)}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
A component name is resolved relative to the component directories, **not relative to the file that calls it**, so the same name resolves to the same component everywhere. When a component is not found, an empty string is returned and `tpl.error` is set (`code: 10000`) instead of throwing. See [Advanced Features](#1-theme-style-and-directory-support) for the full directory chain.
|
|
237
|
+
|
|
238
|
+
### 6. Caching Mechanism
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
// Use cache for view rendering: cache: true is enough, the cache filename is derived from the template path
|
|
242
|
+
const html = tpl.view("./index.html", {
|
|
243
|
+
data: someData
|
|
244
|
+
}, {
|
|
245
|
+
cache: true
|
|
246
|
+
});
|
|
247
|
+
// Cache file: cache/{style}/{lang}/index.cache.html (cache_dims defaults to ['style', 'lang'])
|
|
248
|
+
// e.g. cache/light/zh_cn/index.cache.html, cache/dark/en_us/index.cache.html
|
|
249
|
+
|
|
250
|
+
// You can also pass an explicit cache filename
|
|
251
|
+
tpl.view("./index.html", { data: someData }, {
|
|
252
|
+
cache: true,
|
|
253
|
+
cache_filename: "./index" // Same as ./index.cache.html
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// cache: false only writes the cache without reading it, useful for a forced refresh
|
|
257
|
+
tpl.view("./index.html", { data: someData }, {
|
|
258
|
+
cache: false,
|
|
259
|
+
cache_filename: "./index"
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Clear cache: without arguments only the cache directory of the current instance is cleared
|
|
263
|
+
tpl.clearCache();
|
|
264
|
+
// Clear a specific directory, relative to cache_root
|
|
265
|
+
tpl.clearCache('./game');
|
|
266
|
+
// Clear all cache
|
|
267
|
+
tpl.clearCache('./');
|
|
268
|
+
|
|
269
|
+
// Use getCacheFile to inspect the cache path of the current instance; a string is accepted
|
|
270
|
+
tpl.getCacheFile('./index');
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Cache Count Is Driven by cache_dims
|
|
274
|
+
|
|
275
|
+
`cache_dims` should only contain the dimensions **users can choose**. The division of responsibility in this module is:
|
|
276
|
+
|
|
277
|
+
| Dimension | Level | User-selectable | In cache layering |
|
|
278
|
+
| --- | --- | --- | --- |
|
|
279
|
+
| Theme `theme` | Deployment | No | No, uniform across the site |
|
|
280
|
+
| Style `style` | User | Yes | Yes |
|
|
281
|
+
| Language `lang` | User | Yes | Yes |
|
|
282
|
+
|
|
283
|
+
The default is therefore `['style', 'lang']`: users are grouped by "style + language", and users in the same combination share the same cache.
|
|
284
|
+
|
|
285
|
+
The upper bound on the number of cache files is:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
cache files ≤ (named styles + 1) × languages × cached templates
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The `+ 1` stands for **no style selected** — it is a distinct combination, but the empty string does not occupy a directory level, so its cache sits directly under the language directory (that combination only appears when `default_style` is empty too):
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
cache/dark/zh_cn/test.cache.html ← dark style selected, one extra style level
|
|
295
|
+
cache/zh_cn/test.cache.html ← no style, lang sits directly under the root
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
This is an **upper bound, not a fixed count**: only combinations and templates that are actually rendered produce a cache file, and a combination that is never requested has no file at all. With 2 named styles (dark, light, plus no style = 3 combinations), 2 languages and 20 templates:
|
|
299
|
+
|
|
300
|
+
| `cache_dims` | Levels | Files (upper bound) |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| `['style', 'lang']` (default) | 3 combinations × 2 languages × 20 templates | 120 |
|
|
303
|
+
| `['lang']` | no style dimension, 2 languages × 20 templates | 40 |
|
|
304
|
+
| `[]` | no layering, 20 templates | 20 |
|
|
305
|
+
|
|
306
|
+
The dimensions only take **finite enumerations** such as style and language; a user merely falls into one of those combinations, so this count is **completely independent of the number of users**. With 5,000 users, 1 named style (dark, plus no style = 2 combinations), 2 languages and 2 templates, even when all 4 combinations are used only 8 cache files are produced:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
cache/zh_cn/test.cache.html ← no style + Chinese
|
|
310
|
+
cache/en_us/test.cache.html ← no style + English
|
|
311
|
+
cache/dark/zh_cn/test.cache.html ← dark style + Chinese
|
|
312
|
+
cache/dark/en_us/test.cache.html ← dark style + English
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The module provides no per-user dimension: each entry would serve exactly one user and only hit when "the same user revisits the same page while the data stays unchanged", yet user data (balance, orders, messages) usually changes on every request, so the hit rate approaches zero and stale content may be served. **Pages that contain user data are not suitable for full-page caching**; fetch those regions asynchronously, or use the in-memory cache described below.
|
|
316
|
+
|
|
317
|
+
#### Theme Does Not Participate in Cache Layering
|
|
318
|
+
|
|
319
|
+
A theme is a deployment-level setting, uniform for every user on the site and not switched at runtime, so `theme` is not part of `cache_dims` by default. This comes with one constraint: **switching the theme at runtime reuses the previous theme's cache**. The module logs a warning from `setTheme`; when a switch really is needed, clear the cache or include `theme` in the dimensions:
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
// Option 1: clear the cache after switching the theme
|
|
323
|
+
tpl.setTheme('default');
|
|
324
|
+
tpl.clearCache('./');
|
|
325
|
+
|
|
326
|
+
// Option 2: include the theme when multiple themes coexist long term
|
|
327
|
+
const tpl_multi = new Tpl({
|
|
328
|
+
default_theme: 'game',
|
|
329
|
+
cache_dims: ['theme', 'style', 'lang']
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### Automatic Cache Invalidation
|
|
334
|
+
|
|
335
|
+
After a template file or a language pack file is modified, the old cache is invalidated and rebuilt on the next render — **no manual clearing needed**. The check compares file modification times, not file contents:
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
// Edit a template during development and it takes effect on refresh, no clearCache needed
|
|
339
|
+
// Editing static/lang/zh_cn.json rebuilds the Chinese cache the same way
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Language pack changes are only checked when the cache is layered by language (`cache_dims` contains `lang`); otherwise only the template file is checked.
|
|
343
|
+
|
|
344
|
+
#### In-memory Cache
|
|
345
|
+
|
|
346
|
+
When you want the cache to stay off disk and live only inside the process, use the in-memory cache:
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
const tpl = new Tpl({
|
|
350
|
+
default_theme: 'game',
|
|
351
|
+
cache_type: 'memory', // file (default) for physical files, memory for process memory
|
|
352
|
+
cache_max: 1000 // Max entries in memory; the earliest written entry is evicted beyond it, 0 means unlimited
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
In-memory entries live in `Tpl.cache_store` keyed by the full cache file path and use least-recently-used eviction: a hit moves the entry to the tail, and the earliest written entry is evicted beyond `cache_max`. The cache is cleared on process restart, which means each process keeps its own copy in a multi-instance deployment.
|
|
357
|
+
|
|
358
|
+
`clearCache` clears both physical files and in-memory entries, and its scope is likewise limited by `cache_dims`.
|
|
359
|
+
|
|
360
|
+
### 7. Hook System
|
|
361
|
+
|
|
362
|
+
#### Add Function Hook
|
|
363
|
+
```javascript
|
|
364
|
+
Tpl.global_hook.addFunc('list', async function(viewBag, req, type) {
|
|
365
|
+
// Process list logic
|
|
366
|
+
var query = req.query;
|
|
367
|
+
if (query.user_id) {
|
|
368
|
+
viewBag.user_id = query.user_id;
|
|
369
|
+
}
|
|
370
|
+
return someData;
|
|
371
|
+
}, 100, 'listFunc'); // sort: priority, alias: alias
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### Add Action Hook
|
|
375
|
+
```javascript
|
|
376
|
+
Tpl.global_hook.addAction('header', function(viewBag, param) {
|
|
377
|
+
return "<header>Website Header</header>";
|
|
378
|
+
}, 1, "headerAction");
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Add Filter Hook
|
|
382
|
+
```javascript
|
|
383
|
+
Tpl.global_hook.addFilter('content', function(viewBag, content, param) {
|
|
384
|
+
return content.replace("old text", "new text");
|
|
385
|
+
}, 1, "contentFilter");
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Execute Hooks
|
|
389
|
+
```javascript
|
|
390
|
+
// Execute function hook
|
|
391
|
+
const result = await tpl.runFunc('list', { query: { page: 1 } }, 'article');
|
|
392
|
+
|
|
393
|
+
// Execute action hook
|
|
394
|
+
const actionResult = tpl.runHook('header', { param: 'value' });
|
|
395
|
+
|
|
396
|
+
// Use hooks in template
|
|
397
|
+
const html = tpl.render(`
|
|
398
|
+
<div>
|
|
399
|
+
${hookAction('header')}
|
|
400
|
+
${hookFilter('content', 'original content')}
|
|
401
|
+
</div>
|
|
402
|
+
`, data);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Delete Hooks
|
|
406
|
+
```javascript
|
|
407
|
+
// Delete specific hook
|
|
408
|
+
Tpl.global_hook.delFunc('list', 'listFunc');
|
|
409
|
+
Tpl.global_hook.delAction('header', 'headerAction');
|
|
410
|
+
Tpl.global_hook.delFilter('content', 'contentFilter');
|
|
411
|
+
|
|
412
|
+
// Clear all hooks
|
|
413
|
+
Tpl.global_hook.clear();
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 8. Multi-user Styles and Languages
|
|
417
|
+
|
|
418
|
+
The module works only on the parameters it receives and does not care where they come from. Configuration has two levels:
|
|
419
|
+
|
|
420
|
+
- **Deployment-level**: theme, template directory, cache directory, language pack directory and so on — uniform across the site and held by the master instance
|
|
421
|
+
- **User-level**: style and language, chosen by each user
|
|
422
|
+
|
|
423
|
+
There are two ways to use them: **passing options** (a single instance, with `style` / `lang` passed in the `view` options, no derived instance needed) and **deriving instances** (`fork`). The former is recommended; the following uses `fork` so each option's effect is easy to see. See [Using Styles and Languages Across Users](#6-using-styles-and-languages-across-users) for the trade-offs.
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
// Master instance: holds the deployment-level configuration
|
|
427
|
+
const tpl = new Tpl({
|
|
428
|
+
root: './app/shop/', // application root directory
|
|
429
|
+
default_theme: 'game', // theme is fixed at deployment level, users do not pick it
|
|
430
|
+
default_style: '', // default style when the user picks none
|
|
431
|
+
default_lang: 'zh_cn', // default language when the user picks none
|
|
432
|
+
lang_dir: './static/lang/', // application-level language pack directory, filename is the language code
|
|
433
|
+
langs: {}, // optional in-memory language packs, take precedence over files
|
|
434
|
+
// cache is layered by the user-selectable dimensions: style + language
|
|
435
|
+
cache_dims: ['style', 'lang']
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// User A: no style + Chinese
|
|
439
|
+
const tpl_a = tpl.fork();
|
|
440
|
+
// User B: dark style + English
|
|
441
|
+
const tpl_b = tpl.fork({ style: 'dark', lang: 'en_us' });
|
|
442
|
+
|
|
443
|
+
tpl_a.getTheme(); // "game", inherited from the master instance
|
|
444
|
+
tpl_a.getStyle(); // ""
|
|
445
|
+
tpl_a.getLang(); // "zh_cn"
|
|
446
|
+
|
|
447
|
+
tpl_b.getTheme(); // "game"
|
|
448
|
+
tpl_b.getStyle(); // "dark"
|
|
449
|
+
tpl_b.getLang(); // "en_us"
|
|
450
|
+
|
|
451
|
+
// Render separately; the language and the cache differ per configuration,
|
|
452
|
+
// while template lookup stays identical because templates are not layered by style
|
|
453
|
+
tpl_a.view('./index.html', { title: '首页' });
|
|
454
|
+
tpl_b.view('./index.html', { title: 'Home' });
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Template directories are resolved by priority — `static/diy/` → current theme → default theme — so both user A and user B resolve to `static/template/game/index.html`; the style takes no part in template lookup (see [Advanced Features](#1-theme-style-and-directory-support) for the full order). With `default_theme: 'game'` above, the current theme is also the site default theme, so the chain has just the first two levels; under another current theme the default theme is the third:
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
static/diy/index.html ← user override directory, highest priority, shared by all instances
|
|
461
|
+
static/template/game/index.html ← current theme == default theme: user A and user B hit this very same file
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Only the theme falls back, never the style. What the style changes is the stylesheet list (and the cache path, since `cache_dims` contains `style`), not the template:
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
user A (light style): static/template/game/style/light/light.css + the shared static/template/game/style/base/common.css
|
|
468
|
+
user B (dark style): static/template/game/style/dark/dark.css + the shared static/template/game/style/base/common.css
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Styles are layered by style and collect every css along their own chain, keeping only the highest-priority copy per filename, so dark and light each read their own directory while their shared styles live in the base style directory instead of being duplicated.
|
|
472
|
+
|
|
473
|
+
`$t` is bound to the language of the current instance and can be used in both rendered strings and template files:
|
|
474
|
+
|
|
475
|
+
```html
|
|
476
|
+
<!-- Rendered string -->
|
|
477
|
+
<p>${$t('hello', 'User')}</p>
|
|
478
|
+
<!-- User A (zh_cn) output: <p>你好,User!</p> -->
|
|
479
|
+
<!-- User B (en_us) output: <p>Hello, User!</p> -->
|
|
480
|
+
|
|
481
|
+
<!-- Named interpolation: write {name} in the entry and pass an object -->
|
|
482
|
+
<p>${$t('welcome', { name: user.name })}</p>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Templates can also use `$view` for sub-templates, `$component` for components, `$css` for stylesheets and `$js` for scripts; all of them resolve through their own directory priority chain. The `$css` / `$js` name supports `*` wildcards and returns an array when one is used:
|
|
486
|
+
|
|
487
|
+
```html
|
|
488
|
+
<div>${@$component('bar')}</div>
|
|
489
|
+
<link rel="stylesheet" href="${$css('common.css')}">
|
|
490
|
+
${each $css('*.css') value}
|
|
491
|
+
<link rel="stylesheet" href="${value}">
|
|
492
|
+
${/each}
|
|
493
|
+
|
|
494
|
+
<!-- every script; app* or *promo* narrows by name, and the order is the execution order -->
|
|
495
|
+
${each $js('*.js') value}
|
|
496
|
+
<script src="${value}"></script>
|
|
497
|
+
${/each}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
In server-side code use `t` / `tc`, which are equivalent to `$t` in templates:
|
|
501
|
+
|
|
502
|
+
```javascript
|
|
503
|
+
tpl.t('hello', 'User'); // "你好,User!", replaces {0}, {1} ... placeholders in order
|
|
504
|
+
tpl.t('welcome', { name: '张三' }); // "欢迎,张三!", an object argument fills {name} placeholders
|
|
505
|
+
tpl.t('bar.nav'); // "游戏导航", supports a.b style nested keys
|
|
506
|
+
tpl.tc('items', 1); // Plural lookup, picks the form by language rule: "共 1 项" in Chinese
|
|
507
|
+
tpl.tc('items', 5); // "1 item" / "5 items" in English
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Plural entries are arrays in the language pack, `{count}` is the count placeholder:
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
// static/lang/en_us.json
|
|
514
|
+
{ "items": ["{count} item", "{count} items"] }
|
|
515
|
+
```
|
|
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 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
|
+
|
|
519
|
+
#### Language Pack Files
|
|
520
|
+
|
|
521
|
+
Keep copy in JSON language packs and let template files call `$t` only; do not hardcode copy in templates. The filename is the language code:
|
|
522
|
+
|
|
523
|
+
```
|
|
524
524
|
static/lang/zh_cn.json ← application-level Chinese
|
|
525
525
|
static/lang/en_us.json ← application-level English
|
|
526
526
|
static/template/game/lang/zh_cn.json ← Chinese of the game theme, overrides the same keys
|
|
527
|
-
static/template/game/lang/en_us.json ← English of the game theme, overrides the same keys
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
```json
|
|
531
|
-
// static/lang/zh_cn.json
|
|
532
|
-
{
|
|
533
|
-
"hello": "你好,{0}!",
|
|
534
|
-
"theme": { "game": "游戏主题" },
|
|
535
|
-
"bar": { "nav": "导航栏", "user_id": "用户ID" }
|
|
536
|
-
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
```json
|
|
540
|
-
// static/template/game/lang/zh_cn.json: only the keys to override
|
|
541
|
-
{
|
|
542
|
-
"bar": { "nav": "游戏导航" }
|
|
543
|
-
}
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
```html
|
|
547
|
-
<!-- static/template/game/test.html: templates only resolve entries -->
|
|
548
|
-
<div>${$t('theme.game')}</div>
|
|
549
|
-
<!-- zh_cn output: <div>游戏主题</div> -->
|
|
550
|
-
<!-- en_us output: <div>Game Theme</div> -->
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
Language packs are merged in the following order, each overriding the previous one: application level `lang_dir/{lang}.json` → default theme `{theme_dir}/{default theme}/{theme_lang_dir}/{lang}.json` → current theme `{theme_dir}/{current theme}/{theme_lang_dir}/{lang}.json` → the `langs` option. Entries of the same language are merged by key, and higher priority overrides same-named keys, so the same template can produce different copy per theme.
|
|
554
|
-
|
|
555
|
-
Entries are resolved in this order: current language → default language. When both miss, an error is logged and `undefined` is returned, rendering as an empty string. To get started quickly you may also write copy directly in templates and extract it into language packs later.
|
|
556
|
-
|
|
557
|
-
Language pack files are read when an instance is created and then cached by a file fingerprint (path + mtime): while the files are unchanged, new instances and `fork` reuse the parsed result; after a file is added, edited or removed, the next read reloads automatically. Language packs of already created instances are not refreshed automatically — call `reloadLangs()` for that.
|
|
558
|
-
|
|
559
|
-
```javascript
|
|
560
|
-
tpl.getLangs(); // ['zh_cn', 'en_us']; languages without entries are filtered out
|
|
561
|
-
tpl.reloadLangs(); // Reload by file fingerprint, keeping the current language; returns the language list
|
|
562
|
-
tpl.reloadLangs(true); // Skip the cache and force re-reading the language files
|
|
563
|
-
tpl.loadLang('en_us'); // Reload and return the entries of one language
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
Switch at runtime (only affects the current instance). Style and language are user-selectable; the theme is a deployment-level setting that users should not switch at runtime:
|
|
567
|
-
|
|
568
|
-
```javascript
|
|
569
|
-
tpl_b.setStyle('light'); // Returns the applied style name
|
|
570
|
-
tpl_b.setLang('zh_cn'); // Returns the applied language code
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
Theme and style setters validate that the directory exists. When it does not, a warning is logged and the **previous value is kept**, so a user never believes the switch succeeded while the default value is still in use:
|
|
574
|
-
|
|
575
|
-
```javascript
|
|
576
|
-
// There is no not_exist theme or style directory under static/template
|
|
577
|
-
tpl_b.setStyle('not_exist'); // Returns the old style "dark" and logs a warning
|
|
578
|
-
tpl_b.setTheme('not_exist'); // Returns the old theme "game" and logs a warning (theme validation works the same way)
|
|
579
|
-
|
|
580
|
-
// Skip validation when the directory has not been created yet
|
|
581
|
-
tpl_b.setStyle('not_exist', true); // Returns "not_exist"
|
|
582
|
-
|
|
583
|
-
// An empty string means no style is set; lookup and cache layering fall back to the default style
|
|
584
|
-
tpl_b.setStyle('');
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
The cache is layered by the dimensions in `cache_dims`, and users are grouped by "style + language" so that users in the same combination share the cache:
|
|
588
|
-
|
|
589
|
-
```
|
|
590
|
-
cache/zh_cn/test.cache.html ← user A (no style + Chinese)
|
|
591
|
-
cache/en_us/test.cache.html ← user A switched to English
|
|
592
|
-
cache/dark/zh_cn/test.cache.html ← user B (dark style + Chinese)
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
```javascript
|
|
596
|
-
// Get the cache file path of the current instance
|
|
597
|
-
tpl_a.getCacheFile({ cache_filename: './test' });
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
Cache files are grouped by "style + language", so users sharing a combination share the same cache file and the count is independent of the user count — see [Cache Count Is Driven by cache_dims](#cache-count-is-driven-by-cache_dims).
|
|
601
|
-
|
|
602
|
-
## Advanced Features
|
|
603
|
-
|
|
604
|
-
### 1. Theme, Style and Directory Support
|
|
605
|
-
```javascript
|
|
606
|
-
// Set current style (a variant inside a theme, user-selectable; keeps the old style and logs a warning when missing)
|
|
607
|
-
tpl.setStyle("dark");
|
|
608
|
-
// Clear the style
|
|
609
|
-
tpl.setStyle("");
|
|
610
|
-
|
|
611
|
-
// Set current theme (a deployment-level setting users should not switch at runtime)
|
|
612
|
-
// Keeps the old theme and logs a warning when the directory is missing; clear the cache after switching,
|
|
613
|
-
// see "Theme Does Not Participate in Cache Layering"
|
|
614
|
-
tpl.setTheme("default");
|
|
615
|
-
// Skip directory validation and set anyway
|
|
616
|
-
tpl.setTheme("default", true);
|
|
617
|
-
|
|
618
|
-
// Or assign directly (no validation)
|
|
619
|
-
tpl.current_theme = "default";
|
|
620
|
-
tpl.current_style = "dark";
|
|
621
|
-
|
|
622
|
-
// Template directory search order (priority from high to low; {dir} is this.dir, the application root):
|
|
623
|
-
// 1. {dir}/static/diy/ user override directory
|
|
624
|
-
// 2. {dir}/static/template/{current theme}/ templates organised by feature directly under the theme directory
|
|
625
|
-
// 3. {dir}/static/template/{default theme}/
|
|
626
|
-
//
|
|
627
|
-
// Templates are layered by theme only, never by style: a style is a visual variant and must not change the HTML structure,
|
|
628
|
-
// so switching styles keeps resolving the very same template file
|
|
629
|
-
// The path is relative to the theme directory and may include any feature directory, e.g. ./index.html, ./article/list.html, ./article/detail.html
|
|
630
|
-
// The extension is appended automatically when omitted (extname, '.html' by default), so
|
|
631
|
-
// view('./article/list') and view('./article/list.html') are equivalent
|
|
632
|
-
// Templates override by filename, and the highest-priority file wins (getFile); the style takes no part in this lookup
|
|
633
|
-
// A style that needs a different look changes css; different copy or structure is handled with model variables
|
|
634
|
-
|
|
635
|
-
// Component directory search order (priority from high to low; components use a chain of their own,
|
|
636
|
-
// isomorphic to the template chain with one extra fixed component/ prefix on every level):
|
|
637
|
-
// 1. {dir}/static/diy/component/ user override directory
|
|
638
|
-
// 2. {dir}/static/template/{current theme}/component/
|
|
639
|
-
// 3. {dir}/static/template/{default theme}/component/
|
|
640
|
-
//
|
|
641
|
-
// A component name is resolved relative to the component directories, not relative to the calling file,
|
|
642
|
-
// so the same name resolves identically wherever it is used
|
|
643
|
-
// Names may include sub-directories such as article/card, and the extension is appended automatically (getComponentFile)
|
|
644
|
-
// A component missing from the current theme falls back to the default theme's component directory,
|
|
645
|
-
// and static/diy/component/ overrides every theme globally
|
|
646
|
-
// Components are not cached by default; pass cache and cache_filename explicitly through the options when needed
|
|
647
|
-
|
|
648
|
-
// Style directory search order (priority from high to low): styles are layered by theme and by style,
|
|
649
|
-
// so they carry two more levels (the current style and the base style) than templates
|
|
650
|
-
// 1. {dir}/static/diy/ user override directory
|
|
651
|
-
// 2. {dir}/static/template/{current theme}/style/{current style}/
|
|
652
|
-
// 3. {dir}/static/template/{current theme}/style/{base style}/
|
|
653
|
-
// 4. {dir}/static/template/{default theme}/style/{current style}/
|
|
654
|
-
// 5. {dir}/static/template/{default theme}/style/{base style}/
|
|
655
|
-
//
|
|
656
|
-
// Only the theme falls back, never the style: files of other style directories are not inherited
|
|
657
|
-
// Shared styles belong in the base style directory (base_style, 'base' by default),
|
|
658
|
-
// each style directory only holds its own differences
|
|
659
|
-
// The style directory holds css only and never contains templates
|
|
660
|
-
// Styles collect every css of the directories above, keeping only the highest-priority copy per filename (getCssFiles)
|
|
661
|
-
// With no style set that level uses the default style; when the default style is empty too, the style directory of the theme itself is used
|
|
662
|
-
|
|
663
|
-
// Script directory search order (priority from high to low): layered by theme only, no style or base style level
|
|
664
|
-
// 1. {dir}/static/diy/ user override directory
|
|
665
|
-
// 2. {dir}/static/template/{current theme}/js/
|
|
666
|
-
// 3. {dir}/static/template/{default theme}/js/
|
|
667
|
-
// 4. {dir}/static/js/ application-level shared scripts
|
|
668
|
-
//
|
|
669
|
-
// Scripts are not layered by style: a style is a visual variant, scripts do not change with colour or lightness,
|
|
670
|
-
// so switching styles should not switch scripts
|
|
671
|
-
// Shared scripts belong in the application-level static/js/, and keep being provided there when the theme changes
|
|
672
|
-
|
|
673
|
-
// Style files are returned by ascending priority: the base style comes first as the foundation,
|
|
674
|
-
// the current style overrides after it (a later-loaded rule is the one that takes effect)
|
|
675
|
-
tpl.getCssFiles(); // ['.../static/template/default/style/base/common.css', '.../static/template/default/style/light/light.css', ...]
|
|
676
|
-
|
|
677
|
-
// Scripts are ascending too: application-level shared scripts come first, theme scripts come after (dependency order then holds)
|
|
678
|
-
tpl.getJsFiles(); // ['.../static/js/utils.js', '.../static/template/default/js/app.js', ...]
|
|
679
|
-
|
|
680
|
-
// Single style file: resolved along the same chain, returns its full path or null
|
|
681
|
-
tpl.getCssFile('common.css'); // hits the base style common.css
|
|
682
|
-
tpl.getCssFile('dark.css', 'default', 'dark'); // explicitly read another theme and style
|
|
683
|
-
|
|
684
|
-
// Names support * wildcards: *suffix matches the end, prefix* the start, *infix* anywhere
|
|
685
|
-
tpl.getCssFile('*.css'); // returns an array, same as getCssFiles()
|
|
686
|
-
tpl.matchCssFiles('light*'); // array: only styles named light*
|
|
687
|
-
tpl.matchCssFiles('*common*'); // array: shared styles across directories
|
|
688
|
-
// In templates:
|
|
689
|
-
// <link rel="stylesheet" href="${$css('common.css')}"> exact reference
|
|
690
|
-
// ${each $css('*.css') value}<link href="${value}">${/each} wildcard batch
|
|
691
|
-
// How many to include and in which order is up to the template; the engine only returns paths
|
|
692
|
-
|
|
693
|
-
// Scripts work the same way as styles, just without the style argument; names also support * wildcards
|
|
694
|
-
tpl.getJsFile('utils.js'); // hits the application-level shared script
|
|
695
|
-
tpl.getJsFile('app*'); // an array when a wildcard is used, as above
|
|
696
|
-
tpl.getJsFile('promo.js', 'festival'); // explicitly read a script of one theme, null when absent
|
|
697
|
-
tpl.matchJsFiles('*.js'); // every script, an array, the order is the execution order
|
|
698
|
-
tpl.matchJsFiles('app*'); // prefix match: only scripts starting with app
|
|
699
|
-
tpl.matchJsFiles('*promo*'); // infix match: same-name family across directories
|
|
700
|
-
tpl.matchJsFiles('nope*'); // no match: an empty array [], not null
|
|
701
|
-
tpl.matchJsFiles('promo*', 'festival'); // specify a theme
|
|
702
|
-
// In templates:
|
|
703
|
-
// <script src="${$js('utils.js')}"></script> exact reference
|
|
704
|
-
// ${each $js('*.js') value}<script src="${value}"></script>${/each} wildcard batch
|
|
705
|
-
// ${each $js('app*') value}<script src="${value}"></script>${/each} group by module prefix
|
|
706
|
-
// ${if $js('chart.js')}<script src="${$js('chart.js')}"></script>${/if} skip when absent, avoiding a 404
|
|
707
|
-
//
|
|
708
|
-
// With the site default theme default and the current theme festival, the hit order of $js('*.js') mirrors the layering:
|
|
709
|
-
// ['utils.js' (application static/js/), 'app.js' (default theme, hit by fallback since festival has none), 'promo.js' (current theme static/template/festival/js/), 'patch.js' (static/diy/)]
|
|
710
|
-
|
|
711
|
-
// Resolved theme, template, style and script directories
|
|
712
|
-
tpl.getThemeDir('default'); // {dir}/static/template/default/
|
|
713
|
-
tpl.getTemplateDirs(); // full paths of the template directories, high to low: static/diy/ → current theme → default theme
|
|
714
|
-
tpl.getComponentDirs(); // full paths of the component directories, high to low: static/diy/component → current theme/component → default theme/component
|
|
715
|
-
tpl.getStyleDir('default', 'dark');// {dir}/static/template/default/style/dark/
|
|
716
|
-
tpl.getStyleDirs(); // full paths of the style directories above in priority order; styles only, not used for templates (duplicates are removed when the current and default theme match)
|
|
717
|
-
tpl.getJsDirs(); // full paths of the script directories, high to low: static/diy/ → current theme → default theme → application level
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### 2. Template Rules
|
|
721
|
-
Built-in support for multiple template rules, including:
|
|
722
|
-
- nativeRule: Native syntax rules
|
|
723
|
-
- artRule: art-template syntax rules
|
|
724
|
-
- htmlRule: HTML comment syntax rules
|
|
725
|
-
- jsRule: JavaScript syntax rules
|
|
726
|
-
- pyRule: Python-style syntax rules
|
|
727
|
-
|
|
728
|
-
### 3. Error Handling
|
|
729
|
-
```javascript
|
|
730
|
-
const html = tpl.view("./index.html", data);
|
|
731
|
-
if (tpl.error) {
|
|
732
|
-
console.error("Rendering error:", tpl.error);
|
|
733
|
-
}
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### 4. Character Escaping
|
|
737
|
-
```javascript
|
|
738
|
-
// Automatically escape special characters
|
|
739
|
-
const escaped = tpl.escape("<div>${content}</div>");
|
|
740
|
-
// Output: <div>·$·{content}</div>
|
|
741
|
-
|
|
742
|
-
// Restore escaped characters
|
|
743
|
-
const restored = tpl.escape(escaped, true);
|
|
744
|
-
// Output: <div>${content}</div>
|
|
745
|
-
```
|
|
746
|
-
|
|
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
|
-
|
|
751
|
-
```javascript
|
|
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' } };
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
### 6. Using Styles and Languages Across Users
|
|
763
|
-
|
|
764
|
-
The theme is a deployment-level setting while the style and language are chosen by each user. There are two ways to use them, chosen by whether instance-level state is needed:
|
|
765
|
-
|
|
766
|
-
| | Passing options | Deriving instances |
|
|
767
|
-
| --- | --- | --- |
|
|
768
|
-
| Usage | `tpl.view(file, model, { style, lang })` | `tpl.fork({ style, lang })` |
|
|
769
|
-
| Instances | 1 | one per derived instance |
|
|
770
|
-
| Fits | most cases, no instance management | needs instance-level state such as `viewBag` |
|
|
771
|
-
|
|
772
|
-
#### Option 1: Pass It on Each Render (Recommended)
|
|
773
|
-
|
|
774
|
-
A single instance can serve every user; each render receives the style and language through `options`, and the instance state is restored afterwards:
|
|
775
|
-
|
|
776
|
-
```javascript
|
|
777
|
-
const tpl = new Tpl({
|
|
778
|
-
root: './app/shop/',
|
|
779
|
-
default_theme: 'game', // theme fixed at deployment level, users do not pick it
|
|
780
|
-
lang_dir: './static/lang/',
|
|
781
|
-
cache_dims: ['style', 'lang'] // users are grouped by style + language
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
// User A: dark style + English
|
|
785
|
-
tpl.view('./index.html', { title: 'Home' }, { style: 'dark', lang: 'en_us', cache: true });
|
|
786
|
-
|
|
787
|
-
// User B: no style + Chinese
|
|
788
|
-
tpl.view('./index.html', { title: '首页' }, { lang: 'zh_cn', cache: true });
|
|
789
|
-
|
|
790
|
-
tpl.getStyle(); // "", unaffected by the two renders above
|
|
791
|
-
tpl.getLang(); // "zh_cn"
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
User preferences (a `user_id` → `{style, lang}` mapping) are kept by a store object, exposed on the instance as `store`. The module ships an in-memory store `Memory` and a SQLite store `Sqlite`:
|
|
795
|
-
|
|
796
|
-
```javascript
|
|
797
|
-
tpl.setUser(req.user_id, { style: 'dark', lang: 'en_us' });
|
|
798
|
-
|
|
799
|
-
// A registered user renders with the mapped style and language;
|
|
800
|
-
// an unregistered user falls back to the instance defaults
|
|
801
|
-
tpl.view('./index.html', { title: 'Home' }, tpl.userOptions(req.user_id, { cache: true }));
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
See [User Preference Store](#user-preference-store) for the full API.
|
|
805
|
-
|
|
806
|
-
Cache paths and language follow the passed options, and the cache path is layered by the combination too; template lookup follows the instance theme, since templates are not layered by style:
|
|
807
|
-
|
|
808
|
-
```javascript
|
|
809
|
-
// getCacheFile accepts the same options, handy for inspecting or pre-generating
|
|
810
|
-
tpl.getCacheFile({ cache_filename: './index', style: 'dark', lang: 'en_us' });
|
|
811
|
-
// cache/dark/en_us/index.cache.html
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
`view` renders synchronously: the options are applied on entry and restored on exit, so it is **safe under concurrent requests** and requests never mix.
|
|
815
|
-
|
|
816
|
-
#### Option 2: Derive Instances
|
|
817
|
-
|
|
818
|
-
When instance-level state is needed (for example a template reading `${viewBag.xxx}`), derive instances with `fork`. Deriving by "style + language" lets instances be reused, with the count equal to the number of combinations:
|
|
819
|
-
|
|
820
|
-
```javascript
|
|
821
|
-
const pool = new Map();
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Get the instance for a style and language
|
|
825
|
-
* @param {string} style - Style, an empty string means none selected
|
|
826
|
-
* @param {string} lang - Language
|
|
827
|
-
* @returns {Tpl} Template instance of that combination
|
|
828
|
-
*/
|
|
829
|
-
function getInstance(style, lang) {
|
|
830
|
-
const pool_key = [style || '', lang].join('|');
|
|
831
|
-
if (!pool.has(pool_key)) {
|
|
832
|
-
pool.set(pool_key, tpl.fork({ style: style, lang: lang }));
|
|
833
|
-
}
|
|
834
|
-
return pool.get(pool_key);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// user id → style+language mapping, kept in the built-in mapping table
|
|
838
|
-
// (or maintained by the upper layer if you prefer)
|
|
839
|
-
tpl.setUser(1001, { style: 'dark', lang: 'en_us' });
|
|
840
|
-
const pref = tpl.getUser(1001);
|
|
841
|
-
const instance = getInstance(pref.style, pref.lang);
|
|
842
|
-
const html = instance.view('./index.html', { title: 'Home' }, { cache: true });
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
The instance count is decoupled from the user count and equals the number of combinations actually used; users in the same combination share one instance, so the template lookup result and the cache file are identical.
|
|
846
|
-
|
|
847
|
-
When each user needs a private `viewBag`, derive per user instead (the instance count then equals the user count):
|
|
848
|
-
|
|
849
|
-
```javascript
|
|
850
|
-
const user_inst = tpl.fork({
|
|
851
|
-
user_id: req.user_id,
|
|
852
|
-
style: pref.style,
|
|
853
|
-
lang: pref.lang
|
|
854
|
-
});
|
|
855
|
-
```
|
|
856
|
-
|
|
527
|
+
static/template/game/lang/en_us.json ← English of the game theme, overrides the same keys
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
// static/lang/zh_cn.json
|
|
532
|
+
{
|
|
533
|
+
"hello": "你好,{0}!",
|
|
534
|
+
"theme": { "game": "游戏主题" },
|
|
535
|
+
"bar": { "nav": "导航栏", "user_id": "用户ID" }
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
// static/template/game/lang/zh_cn.json: only the keys to override
|
|
541
|
+
{
|
|
542
|
+
"bar": { "nav": "游戏导航" }
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
```html
|
|
547
|
+
<!-- static/template/game/test.html: templates only resolve entries -->
|
|
548
|
+
<div>${$t('theme.game')}</div>
|
|
549
|
+
<!-- zh_cn output: <div>游戏主题</div> -->
|
|
550
|
+
<!-- en_us output: <div>Game Theme</div> -->
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Language packs are merged in the following order, each overriding the previous one: application level `lang_dir/{lang}.json` → default theme `{theme_dir}/{default theme}/{theme_lang_dir}/{lang}.json` → current theme `{theme_dir}/{current theme}/{theme_lang_dir}/{lang}.json` → the `langs` option. Entries of the same language are merged by key, and higher priority overrides same-named keys, so the same template can produce different copy per theme.
|
|
554
|
+
|
|
555
|
+
Entries are resolved in this order: current language → default language. When both miss, an error is logged and `undefined` is returned, rendering as an empty string. To get started quickly you may also write copy directly in templates and extract it into language packs later.
|
|
556
|
+
|
|
557
|
+
Language pack files are read when an instance is created and then cached by a file fingerprint (path + mtime): while the files are unchanged, new instances and `fork` reuse the parsed result; after a file is added, edited or removed, the next read reloads automatically. Language packs of already created instances are not refreshed automatically — call `reloadLangs()` for that.
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
tpl.getLangs(); // ['zh_cn', 'en_us']; languages without entries are filtered out
|
|
561
|
+
tpl.reloadLangs(); // Reload by file fingerprint, keeping the current language; returns the language list
|
|
562
|
+
tpl.reloadLangs(true); // Skip the cache and force re-reading the language files
|
|
563
|
+
tpl.loadLang('en_us'); // Reload and return the entries of one language
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Switch at runtime (only affects the current instance). Style and language are user-selectable; the theme is a deployment-level setting that users should not switch at runtime:
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
tpl_b.setStyle('light'); // Returns the applied style name
|
|
570
|
+
tpl_b.setLang('zh_cn'); // Returns the applied language code
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Theme and style setters validate that the directory exists. When it does not, a warning is logged and the **previous value is kept**, so a user never believes the switch succeeded while the default value is still in use:
|
|
574
|
+
|
|
575
|
+
```javascript
|
|
576
|
+
// There is no not_exist theme or style directory under static/template
|
|
577
|
+
tpl_b.setStyle('not_exist'); // Returns the old style "dark" and logs a warning
|
|
578
|
+
tpl_b.setTheme('not_exist'); // Returns the old theme "game" and logs a warning (theme validation works the same way)
|
|
579
|
+
|
|
580
|
+
// Skip validation when the directory has not been created yet
|
|
581
|
+
tpl_b.setStyle('not_exist', true); // Returns "not_exist"
|
|
582
|
+
|
|
583
|
+
// An empty string means no style is set; lookup and cache layering fall back to the default style
|
|
584
|
+
tpl_b.setStyle('');
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
The cache is layered by the dimensions in `cache_dims`, and users are grouped by "style + language" so that users in the same combination share the cache:
|
|
588
|
+
|
|
589
|
+
```
|
|
590
|
+
cache/zh_cn/test.cache.html ← user A (no style + Chinese)
|
|
591
|
+
cache/en_us/test.cache.html ← user A switched to English
|
|
592
|
+
cache/dark/zh_cn/test.cache.html ← user B (dark style + Chinese)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
```javascript
|
|
596
|
+
// Get the cache file path of the current instance
|
|
597
|
+
tpl_a.getCacheFile({ cache_filename: './test' });
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Cache files are grouped by "style + language", so users sharing a combination share the same cache file and the count is independent of the user count — see [Cache Count Is Driven by cache_dims](#cache-count-is-driven-by-cache_dims).
|
|
601
|
+
|
|
602
|
+
## Advanced Features
|
|
603
|
+
|
|
604
|
+
### 1. Theme, Style and Directory Support
|
|
605
|
+
```javascript
|
|
606
|
+
// Set current style (a variant inside a theme, user-selectable; keeps the old style and logs a warning when missing)
|
|
607
|
+
tpl.setStyle("dark");
|
|
608
|
+
// Clear the style
|
|
609
|
+
tpl.setStyle("");
|
|
610
|
+
|
|
611
|
+
// Set current theme (a deployment-level setting users should not switch at runtime)
|
|
612
|
+
// Keeps the old theme and logs a warning when the directory is missing; clear the cache after switching,
|
|
613
|
+
// see "Theme Does Not Participate in Cache Layering"
|
|
614
|
+
tpl.setTheme("default");
|
|
615
|
+
// Skip directory validation and set anyway
|
|
616
|
+
tpl.setTheme("default", true);
|
|
617
|
+
|
|
618
|
+
// Or assign directly (no validation)
|
|
619
|
+
tpl.current_theme = "default";
|
|
620
|
+
tpl.current_style = "dark";
|
|
621
|
+
|
|
622
|
+
// Template directory search order (priority from high to low; {dir} is this.dir, the application root):
|
|
623
|
+
// 1. {dir}/static/diy/ user override directory
|
|
624
|
+
// 2. {dir}/static/template/{current theme}/ templates organised by feature directly under the theme directory
|
|
625
|
+
// 3. {dir}/static/template/{default theme}/
|
|
626
|
+
//
|
|
627
|
+
// Templates are layered by theme only, never by style: a style is a visual variant and must not change the HTML structure,
|
|
628
|
+
// so switching styles keeps resolving the very same template file
|
|
629
|
+
// The path is relative to the theme directory and may include any feature directory, e.g. ./index.html, ./article/list.html, ./article/detail.html
|
|
630
|
+
// The extension is appended automatically when omitted (extname, '.html' by default), so
|
|
631
|
+
// view('./article/list') and view('./article/list.html') are equivalent
|
|
632
|
+
// Templates override by filename, and the highest-priority file wins (getFile); the style takes no part in this lookup
|
|
633
|
+
// A style that needs a different look changes css; different copy or structure is handled with model variables
|
|
634
|
+
|
|
635
|
+
// Component directory search order (priority from high to low; components use a chain of their own,
|
|
636
|
+
// isomorphic to the template chain with one extra fixed component/ prefix on every level):
|
|
637
|
+
// 1. {dir}/static/diy/component/ user override directory
|
|
638
|
+
// 2. {dir}/static/template/{current theme}/component/
|
|
639
|
+
// 3. {dir}/static/template/{default theme}/component/
|
|
640
|
+
//
|
|
641
|
+
// A component name is resolved relative to the component directories, not relative to the calling file,
|
|
642
|
+
// so the same name resolves identically wherever it is used
|
|
643
|
+
// Names may include sub-directories such as article/card, and the extension is appended automatically (getComponentFile)
|
|
644
|
+
// A component missing from the current theme falls back to the default theme's component directory,
|
|
645
|
+
// and static/diy/component/ overrides every theme globally
|
|
646
|
+
// Components are not cached by default; pass cache and cache_filename explicitly through the options when needed
|
|
647
|
+
|
|
648
|
+
// Style directory search order (priority from high to low): styles are layered by theme and by style,
|
|
649
|
+
// so they carry two more levels (the current style and the base style) than templates
|
|
650
|
+
// 1. {dir}/static/diy/ user override directory
|
|
651
|
+
// 2. {dir}/static/template/{current theme}/style/{current style}/
|
|
652
|
+
// 3. {dir}/static/template/{current theme}/style/{base style}/
|
|
653
|
+
// 4. {dir}/static/template/{default theme}/style/{current style}/
|
|
654
|
+
// 5. {dir}/static/template/{default theme}/style/{base style}/
|
|
655
|
+
//
|
|
656
|
+
// Only the theme falls back, never the style: files of other style directories are not inherited
|
|
657
|
+
// Shared styles belong in the base style directory (base_style, 'base' by default),
|
|
658
|
+
// each style directory only holds its own differences
|
|
659
|
+
// The style directory holds css only and never contains templates
|
|
660
|
+
// Styles collect every css of the directories above, keeping only the highest-priority copy per filename (getCssFiles)
|
|
661
|
+
// With no style set that level uses the default style; when the default style is empty too, the style directory of the theme itself is used
|
|
662
|
+
|
|
663
|
+
// Script directory search order (priority from high to low): layered by theme only, no style or base style level
|
|
664
|
+
// 1. {dir}/static/diy/ user override directory
|
|
665
|
+
// 2. {dir}/static/template/{current theme}/js/
|
|
666
|
+
// 3. {dir}/static/template/{default theme}/js/
|
|
667
|
+
// 4. {dir}/static/js/ application-level shared scripts
|
|
668
|
+
//
|
|
669
|
+
// Scripts are not layered by style: a style is a visual variant, scripts do not change with colour or lightness,
|
|
670
|
+
// so switching styles should not switch scripts
|
|
671
|
+
// Shared scripts belong in the application-level static/js/, and keep being provided there when the theme changes
|
|
672
|
+
|
|
673
|
+
// Style files are returned by ascending priority: the base style comes first as the foundation,
|
|
674
|
+
// the current style overrides after it (a later-loaded rule is the one that takes effect)
|
|
675
|
+
tpl.getCssFiles(); // ['.../static/template/default/style/base/common.css', '.../static/template/default/style/light/light.css', ...]
|
|
676
|
+
|
|
677
|
+
// Scripts are ascending too: application-level shared scripts come first, theme scripts come after (dependency order then holds)
|
|
678
|
+
tpl.getJsFiles(); // ['.../static/js/utils.js', '.../static/template/default/js/app.js', ...]
|
|
679
|
+
|
|
680
|
+
// Single style file: resolved along the same chain, returns its full path or null
|
|
681
|
+
tpl.getCssFile('common.css'); // hits the base style common.css
|
|
682
|
+
tpl.getCssFile('dark.css', 'default', 'dark'); // explicitly read another theme and style
|
|
683
|
+
|
|
684
|
+
// Names support * wildcards: *suffix matches the end, prefix* the start, *infix* anywhere
|
|
685
|
+
tpl.getCssFile('*.css'); // returns an array, same as getCssFiles()
|
|
686
|
+
tpl.matchCssFiles('light*'); // array: only styles named light*
|
|
687
|
+
tpl.matchCssFiles('*common*'); // array: shared styles across directories
|
|
688
|
+
// In templates:
|
|
689
|
+
// <link rel="stylesheet" href="${$css('common.css')}"> exact reference
|
|
690
|
+
// ${each $css('*.css') value}<link href="${value}">${/each} wildcard batch
|
|
691
|
+
// How many to include and in which order is up to the template; the engine only returns paths
|
|
692
|
+
|
|
693
|
+
// Scripts work the same way as styles, just without the style argument; names also support * wildcards
|
|
694
|
+
tpl.getJsFile('utils.js'); // hits the application-level shared script
|
|
695
|
+
tpl.getJsFile('app*'); // an array when a wildcard is used, as above
|
|
696
|
+
tpl.getJsFile('promo.js', 'festival'); // explicitly read a script of one theme, null when absent
|
|
697
|
+
tpl.matchJsFiles('*.js'); // every script, an array, the order is the execution order
|
|
698
|
+
tpl.matchJsFiles('app*'); // prefix match: only scripts starting with app
|
|
699
|
+
tpl.matchJsFiles('*promo*'); // infix match: same-name family across directories
|
|
700
|
+
tpl.matchJsFiles('nope*'); // no match: an empty array [], not null
|
|
701
|
+
tpl.matchJsFiles('promo*', 'festival'); // specify a theme
|
|
702
|
+
// In templates:
|
|
703
|
+
// <script src="${$js('utils.js')}"></script> exact reference
|
|
704
|
+
// ${each $js('*.js') value}<script src="${value}"></script>${/each} wildcard batch
|
|
705
|
+
// ${each $js('app*') value}<script src="${value}"></script>${/each} group by module prefix
|
|
706
|
+
// ${if $js('chart.js')}<script src="${$js('chart.js')}"></script>${/if} skip when absent, avoiding a 404
|
|
707
|
+
//
|
|
708
|
+
// With the site default theme default and the current theme festival, the hit order of $js('*.js') mirrors the layering:
|
|
709
|
+
// ['utils.js' (application static/js/), 'app.js' (default theme, hit by fallback since festival has none), 'promo.js' (current theme static/template/festival/js/), 'patch.js' (static/diy/)]
|
|
710
|
+
|
|
711
|
+
// Resolved theme, template, style and script directories
|
|
712
|
+
tpl.getThemeDir('default'); // {dir}/static/template/default/
|
|
713
|
+
tpl.getTemplateDirs(); // full paths of the template directories, high to low: static/diy/ → current theme → default theme
|
|
714
|
+
tpl.getComponentDirs(); // full paths of the component directories, high to low: static/diy/component → current theme/component → default theme/component
|
|
715
|
+
tpl.getStyleDir('default', 'dark');// {dir}/static/template/default/style/dark/
|
|
716
|
+
tpl.getStyleDirs(); // full paths of the style directories above in priority order; styles only, not used for templates (duplicates are removed when the current and default theme match)
|
|
717
|
+
tpl.getJsDirs(); // full paths of the script directories, high to low: static/diy/ → current theme → default theme → application level
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### 2. Template Rules
|
|
721
|
+
Built-in support for multiple template rules, including:
|
|
722
|
+
- nativeRule: Native syntax rules
|
|
723
|
+
- artRule: art-template syntax rules
|
|
724
|
+
- htmlRule: HTML comment syntax rules
|
|
725
|
+
- jsRule: JavaScript syntax rules
|
|
726
|
+
- pyRule: Python-style syntax rules
|
|
727
|
+
|
|
728
|
+
### 3. Error Handling
|
|
729
|
+
```javascript
|
|
730
|
+
const html = tpl.view("./index.html", data);
|
|
731
|
+
if (tpl.error) {
|
|
732
|
+
console.error("Rendering error:", tpl.error);
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### 4. Character Escaping
|
|
737
|
+
```javascript
|
|
738
|
+
// Automatically escape special characters
|
|
739
|
+
const escaped = tpl.escape("<div>${content}</div>");
|
|
740
|
+
// Output: <div>·$·{content}</div>
|
|
741
|
+
|
|
742
|
+
// Restore escaped characters
|
|
743
|
+
const restored = tpl.escape(escaped, true);
|
|
744
|
+
// Output: <div>${content}</div>
|
|
745
|
+
```
|
|
746
|
+
|
|
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
|
+
|
|
751
|
+
```javascript
|
|
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' } };
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### 6. Using Styles and Languages Across Users
|
|
763
|
+
|
|
764
|
+
The theme is a deployment-level setting while the style and language are chosen by each user. There are two ways to use them, chosen by whether instance-level state is needed:
|
|
765
|
+
|
|
766
|
+
| | Passing options | Deriving instances |
|
|
767
|
+
| --- | --- | --- |
|
|
768
|
+
| Usage | `tpl.view(file, model, { style, lang })` | `tpl.fork({ style, lang })` |
|
|
769
|
+
| Instances | 1 | one per derived instance |
|
|
770
|
+
| Fits | most cases, no instance management | needs instance-level state such as `viewBag` |
|
|
771
|
+
|
|
772
|
+
#### Option 1: Pass It on Each Render (Recommended)
|
|
773
|
+
|
|
774
|
+
A single instance can serve every user; each render receives the style and language through `options`, and the instance state is restored afterwards:
|
|
775
|
+
|
|
776
|
+
```javascript
|
|
777
|
+
const tpl = new Tpl({
|
|
778
|
+
root: './app/shop/',
|
|
779
|
+
default_theme: 'game', // theme fixed at deployment level, users do not pick it
|
|
780
|
+
lang_dir: './static/lang/',
|
|
781
|
+
cache_dims: ['style', 'lang'] // users are grouped by style + language
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// User A: dark style + English
|
|
785
|
+
tpl.view('./index.html', { title: 'Home' }, { style: 'dark', lang: 'en_us', cache: true });
|
|
786
|
+
|
|
787
|
+
// User B: no style + Chinese
|
|
788
|
+
tpl.view('./index.html', { title: '首页' }, { lang: 'zh_cn', cache: true });
|
|
789
|
+
|
|
790
|
+
tpl.getStyle(); // "", unaffected by the two renders above
|
|
791
|
+
tpl.getLang(); // "zh_cn"
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
User preferences (a `user_id` → `{style, lang}` mapping) are kept by a store object, exposed on the instance as `store`. The module ships an in-memory store `Memory` and a SQLite store `Sqlite`:
|
|
795
|
+
|
|
796
|
+
```javascript
|
|
797
|
+
tpl.setUser(req.user_id, { style: 'dark', lang: 'en_us' });
|
|
798
|
+
|
|
799
|
+
// A registered user renders with the mapped style and language;
|
|
800
|
+
// an unregistered user falls back to the instance defaults
|
|
801
|
+
tpl.view('./index.html', { title: 'Home' }, tpl.userOptions(req.user_id, { cache: true }));
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
See [User Preference Store](#user-preference-store) for the full API.
|
|
805
|
+
|
|
806
|
+
Cache paths and language follow the passed options, and the cache path is layered by the combination too; template lookup follows the instance theme, since templates are not layered by style:
|
|
807
|
+
|
|
808
|
+
```javascript
|
|
809
|
+
// getCacheFile accepts the same options, handy for inspecting or pre-generating
|
|
810
|
+
tpl.getCacheFile({ cache_filename: './index', style: 'dark', lang: 'en_us' });
|
|
811
|
+
// cache/dark/en_us/index.cache.html
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
`view` renders synchronously: the options are applied on entry and restored on exit, so it is **safe under concurrent requests** and requests never mix.
|
|
815
|
+
|
|
816
|
+
#### Option 2: Derive Instances
|
|
817
|
+
|
|
818
|
+
When instance-level state is needed (for example a template reading `${viewBag.xxx}`), derive instances with `fork`. Deriving by "style + language" lets instances be reused, with the count equal to the number of combinations:
|
|
819
|
+
|
|
820
|
+
```javascript
|
|
821
|
+
const pool = new Map();
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Get the instance for a style and language
|
|
825
|
+
* @param {string} style - Style, an empty string means none selected
|
|
826
|
+
* @param {string} lang - Language
|
|
827
|
+
* @returns {Tpl} Template instance of that combination
|
|
828
|
+
*/
|
|
829
|
+
function getInstance(style, lang) {
|
|
830
|
+
const pool_key = [style || '', lang].join('|');
|
|
831
|
+
if (!pool.has(pool_key)) {
|
|
832
|
+
pool.set(pool_key, tpl.fork({ style: style, lang: lang }));
|
|
833
|
+
}
|
|
834
|
+
return pool.get(pool_key);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// user id → style+language mapping, kept in the built-in mapping table
|
|
838
|
+
// (or maintained by the upper layer if you prefer)
|
|
839
|
+
tpl.setUser(1001, { style: 'dark', lang: 'en_us' });
|
|
840
|
+
const pref = tpl.getUser(1001);
|
|
841
|
+
const instance = getInstance(pref.style, pref.lang);
|
|
842
|
+
const html = instance.view('./index.html', { title: 'Home' }, { cache: true });
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
The instance count is decoupled from the user count and equals the number of combinations actually used; users in the same combination share one instance, so the template lookup result and the cache file are identical.
|
|
846
|
+
|
|
847
|
+
When each user needs a private `viewBag`, derive per user instead (the instance count then equals the user count):
|
|
848
|
+
|
|
849
|
+
```javascript
|
|
850
|
+
const user_inst = tpl.fork({
|
|
851
|
+
user_id: req.user_id,
|
|
852
|
+
style: pref.style,
|
|
853
|
+
lang: pref.lang
|
|
854
|
+
});
|
|
855
|
+
```
|
|
856
|
+
|
|
857
857
|
#### User Preference Store
|
|
858
858
|
|
|
859
859
|
The store maps a `user_id` to the user's style and language. It is exposed on the instance as `store`, and the module exports two built-in implementations, `Memory` and `Sqlite`:
|
|
@@ -924,138 +924,138 @@ Conventions to keep in mind:
|
|
|
924
924
|
- The in-memory store dies with the process, so inject shared storage (such as `Sqlite`) for multi-process or multi-machine deployments, otherwise each process sees only its own data
|
|
925
925
|
- `Sqlite` relies on the built-in `node:sqlite` (available since Node 22.5); on older versions the module simply does not export `Sqlite` and everything else keeps working
|
|
926
926
|
|
|
927
|
-
#### viewBag Is Shared at Instance Level
|
|
928
|
-
|
|
929
|
-
`viewBag` lives on the instance and is shared by every request that uses it, so **never write user-level data into `viewBag`**.
|
|
930
|
-
|
|
931
|
-
**1. User data must be passed through `model`**
|
|
932
|
-
|
|
933
|
-
Data written into `viewBag` is left behind for later requests using the same instance:
|
|
934
|
-
|
|
935
|
-
```javascript
|
|
936
|
-
// Wrong: the hook writes request data into viewBag, and later requests on the same instance read it
|
|
937
|
-
Tpl.global_hook.addFunc('login', async function (m, req) {
|
|
938
|
-
m.user_id = req.user_id; // m is this.viewBag
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// Right: pass it through model on each render
|
|
942
|
-
tpl.view('./index.html', { user_id: req.user_id, title: 'Home' });
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
**2. Templates should read `${user_id}`, not `${viewBag.user_id}`**
|
|
946
|
-
|
|
947
|
-
They read from different sources: `model` belongs to the current request while `viewBag` is instance-level and shared:
|
|
948
|
-
|
|
949
|
-
```html
|
|
950
|
-
<div>${user_id}</div> <!-- the current request's model, safe -->
|
|
951
|
-
<div>${viewBag.user_id}</div> <!-- instance-level shared data, leaks across users -->
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
`viewBag` is only suitable for site-level data that is identical for every user (such as the system name). This holds both for passing options and for deriving instances by combination; only when one separate instance is derived per user is `viewBag` private to that user, and only then does the "hook writes into `viewBag`" data flow work.
|
|
955
|
-
|
|
956
|
-
#### Why the Cache File Count Stays Bounded
|
|
957
|
-
|
|
958
|
-
Styles and languages are finite enumerations and users simply fall into one combination, so the cache file count is independent of the number of users:
|
|
959
|
-
|
|
960
|
-
```
|
|
961
|
-
cache files ≤ (named styles + 1) × languages × cached templates
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
With 1 named style (dark, plus no style = 2 combinations), 2 languages and 20 templates, the upper bound is 80 files (80 = 2 combinations × 2 languages × 20 templates). Whether the site has 10 users or 10,000, that number does not change, because users merely fall into those combinations; combinations that are never requested produce no file.
|
|
965
|
-
|
|
966
|
-
Conversely, a cache must **never hold data that varies per user**: caching data that changes means a user who tops up their balance and refreshes still sees the old value — **that is a correctness problem, not a performance one**.
|
|
967
|
-
|
|
968
|
-
So the rule is simple: **when the rendered result depends on user data, full-page caching should not be done at all**. There are three directions instead:
|
|
969
|
-
|
|
970
|
-
1. Let the front end fetch user-specific regions asynchronously, and cache the page body by style and language;
|
|
971
|
-
2. If per-user separation is genuinely required, pass an explicit `cache_filename` (such as `'u_' + user_id + '_index'`), or switch to the in-memory cache (`cache_type: 'memory'`) with a small `cache_max` and a short lifetime;
|
|
972
|
-
3. Do not cache at all.
|
|
973
|
-
|
|
974
|
-
#### Dimension Order Determines Clearing Granularity
|
|
975
|
-
|
|
976
|
-
The order of `cache_dims` is the directory order, which in turn determines which dimension you can clear in batch. With the default `['style', 'lang']` the directory is `cache/{style}/{lang}/`:
|
|
977
|
-
|
|
978
|
-
```javascript
|
|
979
|
-
// Clear by style: removes the cache of every language under the dark style at once
|
|
980
|
-
tpl.clearCache('./dark');
|
|
981
|
-
|
|
982
|
-
// Users with no style keep their cache under the language directory at the root
|
|
983
|
-
tpl.clearCache('./zh_cn');
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
### 7. Template Function Table and Instance-level Hooks
|
|
987
|
-
|
|
988
|
-
#### Template Function Table
|
|
989
|
-
|
|
990
|
-
Register template functions with `setFunc`; they are injected into the model on every render and can be called by name inside templates. `removeFunc` deletes one and `getFuncs` returns them all:
|
|
991
|
-
|
|
992
|
-
```javascript
|
|
993
|
-
tpl.setFunc('$md', function (text) {
|
|
994
|
-
return text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
tpl.render('${$md(\'**bold**\')}'); // <b>bold</b>
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
Precedence: **a field of the same name in this call's `model` > built-in functions (`$view` / `$t` / `$css` / `$js` / `$component` / `hookAction` / `hookFilter`) > registered functions** — a registered function can neither override a built-in function nor shadow data passed in with the current request. `fork` copies the table to the derived instance.
|
|
1001
|
-
|
|
1002
|
-
#### Instance-level Hooks
|
|
1003
|
-
|
|
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.
|
|
1005
|
-
|
|
1006
|
-
```javascript
|
|
1007
|
-
tpl.hook.addAction('header', function (m) { return '<header>instance-level</header>'; });
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
|
-
## API Reference
|
|
1011
|
-
|
|
1012
|
-
### Tpl Class Methods
|
|
1013
|
-
|
|
1014
|
-
- `constructor(config)` - Constructor, create template instance
|
|
1015
|
-
- `setConfig(config)` - Set configuration parameters (rebuilds the language instance)
|
|
1016
|
-
- `render(body, model, options)` - Render a template string; `options.style` / `options.lang` apply to this render only
|
|
1017
|
-
- `compile(body, options)` - Compile template to function
|
|
1018
|
-
- `view(file, model, options)` - Render template file; `options.style` / `options.lang` apply to this render only and are restored afterwards; `options.dir` restricts the lookup to that directory (for templates outside the theme system such as mail or print templates)
|
|
1019
|
-
- `getFile(file, options)` - Find a template file by directory priority, returns the full path of the hit; `options.dir` restricts the lookup to that directory
|
|
1020
|
-
- `getComponentFile(name, options)` - Find a component file along the component directory chain, returns the full path of the hit; `name` is the component name and may include sub-directories such as `article/card`, with the extension appended from `extname` when omitted; `options.dir` restricts the lookup to that directory
|
|
1021
|
-
- `component(name, model, options)` - Render a component and return the rendered string; the component name is resolved relative to the component directories, not relative to the calling file; when the component is missing an empty string is returned and `error` is set (`code: 10000`), and when `component_dir` is empty `code: 10001` is set — neither throws. `$component(name, model?)` in templates is equivalent
|
|
1022
|
-
- `getCssFiles()` - Get the style files of the current style as an array of full paths in ascending priority order, keeping only the highest-priority copy per filename; the base style comes first as the foundation, the current style and `static/diy/` override after it
|
|
1023
|
-
- `matchCssFiles(pattern, theme, style)` - Match style files by wildcard and return an array of full paths; `pattern` supports `*.css`, `blue*`, `*common*`, and defaults to `*.css`
|
|
1024
|
-
- `getCssFile(name, theme, style)` - Find style files by directory priority; returns an array when `name` contains `*` (empty array when nothing matches), otherwise a single path or `null`. `theme` / `style` default to the current values, so styles of another theme or style can be referenced
|
|
1025
|
-
- `getJsFiles()` - Get the script files as an array of full paths in ascending priority order, keeping only the highest-priority copy per filename; the application level comes first, the theme level and `static/diy/` after it
|
|
1026
|
-
- `matchJsFiles(pattern, theme)` - Match script files by wildcard and return an array of full paths; `pattern` supports `*.js`, `app*`, `*promo*`, and defaults to `*.js`; an empty array when nothing matches
|
|
1027
|
-
- `getJsFile(name, theme)` - Find script files by directory priority, returning the full path; returns an array when `name` contains `*`, otherwise a single path or `null` when absent
|
|
1028
|
-
- `set(obj)` - Set view data
|
|
1029
|
-
- `get(key)` - Get view data
|
|
1030
|
-
- `del(keys)` - Delete view data
|
|
1031
|
-
- `clearCache(path, file)` - Clear cache; `path` is relative to `cache_root`, omit it to clear only the dimension directory of the current instance, pass `'./'` to clear everything
|
|
1032
|
-
- `getCacheFile(options)` - Get the layered cache file path; the argument may be a config object or a cache filename string, and `options.style` / `options.lang` make it compute for that combination
|
|
1033
|
-
- `runFunc(name, req, ...args)` - Execute function hooks; returns a Promise. Both global and instance-level hooks run, with the instance-level return value taking precedence
|
|
1034
|
-
- `runHook(name, req, ...args)` - Execute function hooks (alias of runFunc)
|
|
1035
|
-
- `escape(body, restore)` - Escape/restore characters
|
|
1036
|
-
- `setFunc(name, func)` - Register a template function injected into the model on every render for templates to call; built-in functions and fields of the same name in this call's `model` take precedence
|
|
1037
|
-
- `removeFunc(name)` - Delete a registered template function, returns whether it existed and was removed
|
|
1038
|
-
- `getFuncs()` - Get the template function table
|
|
1039
|
-
|
|
1040
|
-
### Tpl Multi-user Methods
|
|
1041
|
-
|
|
1042
|
-
- `fork(config)` - Derive an instance; the user-level parameters are `{style, lang}`, while deployment-level settings (theme, directories, language packs, etc.) are inherited from the master instance; when only style and language are needed, pass them in the `view` options instead of deriving an instance
|
|
1043
|
-
- `getTheme()` - Get the current theme
|
|
1044
|
-
- `setTheme(theme, force)` - Set the current theme (a deployment-level setting users should not switch at runtime); keeps the old value and logs a warning when the directory is missing, `force: true` skips validation; also warns when `cache_dims` excludes `theme`
|
|
1045
|
-
- `getStyle()` - Get the current style, returns an empty string when unset
|
|
1046
|
-
- `setStyle(style, force)` - Set the current style (user-selectable); an empty string means no style is set (lookup and cache fall back to the default style); keeps the old value and logs a warning when the directory is missing, `force: true` skips validation
|
|
1047
|
-
- `getThemeDir(theme)` - Get the full path of a theme directory
|
|
1048
|
-
- `getStyleDir(theme, style)` - Get the full path of a style directory under a theme; when `style` is empty the style directory of that theme is returned
|
|
927
|
+
#### viewBag Is Shared at Instance Level
|
|
928
|
+
|
|
929
|
+
`viewBag` lives on the instance and is shared by every request that uses it, so **never write user-level data into `viewBag`**.
|
|
930
|
+
|
|
931
|
+
**1. User data must be passed through `model`**
|
|
932
|
+
|
|
933
|
+
Data written into `viewBag` is left behind for later requests using the same instance:
|
|
934
|
+
|
|
935
|
+
```javascript
|
|
936
|
+
// Wrong: the hook writes request data into viewBag, and later requests on the same instance read it
|
|
937
|
+
Tpl.global_hook.addFunc('login', async function (m, req) {
|
|
938
|
+
m.user_id = req.user_id; // m is this.viewBag
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Right: pass it through model on each render
|
|
942
|
+
tpl.view('./index.html', { user_id: req.user_id, title: 'Home' });
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**2. Templates should read `${user_id}`, not `${viewBag.user_id}`**
|
|
946
|
+
|
|
947
|
+
They read from different sources: `model` belongs to the current request while `viewBag` is instance-level and shared:
|
|
948
|
+
|
|
949
|
+
```html
|
|
950
|
+
<div>${user_id}</div> <!-- the current request's model, safe -->
|
|
951
|
+
<div>${viewBag.user_id}</div> <!-- instance-level shared data, leaks across users -->
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
`viewBag` is only suitable for site-level data that is identical for every user (such as the system name). This holds both for passing options and for deriving instances by combination; only when one separate instance is derived per user is `viewBag` private to that user, and only then does the "hook writes into `viewBag`" data flow work.
|
|
955
|
+
|
|
956
|
+
#### Why the Cache File Count Stays Bounded
|
|
957
|
+
|
|
958
|
+
Styles and languages are finite enumerations and users simply fall into one combination, so the cache file count is independent of the number of users:
|
|
959
|
+
|
|
960
|
+
```
|
|
961
|
+
cache files ≤ (named styles + 1) × languages × cached templates
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
With 1 named style (dark, plus no style = 2 combinations), 2 languages and 20 templates, the upper bound is 80 files (80 = 2 combinations × 2 languages × 20 templates). Whether the site has 10 users or 10,000, that number does not change, because users merely fall into those combinations; combinations that are never requested produce no file.
|
|
965
|
+
|
|
966
|
+
Conversely, a cache must **never hold data that varies per user**: caching data that changes means a user who tops up their balance and refreshes still sees the old value — **that is a correctness problem, not a performance one**.
|
|
967
|
+
|
|
968
|
+
So the rule is simple: **when the rendered result depends on user data, full-page caching should not be done at all**. There are three directions instead:
|
|
969
|
+
|
|
970
|
+
1. Let the front end fetch user-specific regions asynchronously, and cache the page body by style and language;
|
|
971
|
+
2. If per-user separation is genuinely required, pass an explicit `cache_filename` (such as `'u_' + user_id + '_index'`), or switch to the in-memory cache (`cache_type: 'memory'`) with a small `cache_max` and a short lifetime;
|
|
972
|
+
3. Do not cache at all.
|
|
973
|
+
|
|
974
|
+
#### Dimension Order Determines Clearing Granularity
|
|
975
|
+
|
|
976
|
+
The order of `cache_dims` is the directory order, which in turn determines which dimension you can clear in batch. With the default `['style', 'lang']` the directory is `cache/{style}/{lang}/`:
|
|
977
|
+
|
|
978
|
+
```javascript
|
|
979
|
+
// Clear by style: removes the cache of every language under the dark style at once
|
|
980
|
+
tpl.clearCache('./dark');
|
|
981
|
+
|
|
982
|
+
// Users with no style keep their cache under the language directory at the root
|
|
983
|
+
tpl.clearCache('./zh_cn');
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### 7. Template Function Table and Instance-level Hooks
|
|
987
|
+
|
|
988
|
+
#### Template Function Table
|
|
989
|
+
|
|
990
|
+
Register template functions with `setFunc`; they are injected into the model on every render and can be called by name inside templates. `removeFunc` deletes one and `getFuncs` returns them all:
|
|
991
|
+
|
|
992
|
+
```javascript
|
|
993
|
+
tpl.setFunc('$md', function (text) {
|
|
994
|
+
return text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
tpl.render('${$md(\'**bold**\')}'); // <b>bold</b>
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
Precedence: **a field of the same name in this call's `model` > built-in functions (`$view` / `$t` / `$css` / `$js` / `$component` / `hookAction` / `hookFilter`) > registered functions** — a registered function can neither override a built-in function nor shadow data passed in with the current request. `fork` copies the table to the derived instance.
|
|
1001
|
+
|
|
1002
|
+
#### Instance-level Hooks
|
|
1003
|
+
|
|
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.
|
|
1005
|
+
|
|
1006
|
+
```javascript
|
|
1007
|
+
tpl.hook.addAction('header', function (m) { return '<header>instance-level</header>'; });
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
## API Reference
|
|
1011
|
+
|
|
1012
|
+
### Tpl Class Methods
|
|
1013
|
+
|
|
1014
|
+
- `constructor(config)` - Constructor, create template instance
|
|
1015
|
+
- `setConfig(config)` - Set configuration parameters (rebuilds the language instance)
|
|
1016
|
+
- `render(body, model, options)` - Render a template string; `options.style` / `options.lang` apply to this render only
|
|
1017
|
+
- `compile(body, options)` - Compile template to function
|
|
1018
|
+
- `view(file, model, options)` - Render template file; `options.style` / `options.lang` apply to this render only and are restored afterwards; `options.dir` restricts the lookup to that directory (for templates outside the theme system such as mail or print templates)
|
|
1019
|
+
- `getFile(file, options)` - Find a template file by directory priority, returns the full path of the hit; `options.dir` restricts the lookup to that directory
|
|
1020
|
+
- `getComponentFile(name, options)` - Find a component file along the component directory chain, returns the full path of the hit; `name` is the component name and may include sub-directories such as `article/card`, with the extension appended from `extname` when omitted; `options.dir` restricts the lookup to that directory
|
|
1021
|
+
- `component(name, model, options)` - Render a component and return the rendered string; the component name is resolved relative to the component directories, not relative to the calling file; when the component is missing an empty string is returned and `error` is set (`code: 10000`), and when `component_dir` is empty `code: 10001` is set — neither throws. `$component(name, model?)` in templates is equivalent
|
|
1022
|
+
- `getCssFiles()` - Get the style files of the current style as an array of full paths in ascending priority order, keeping only the highest-priority copy per filename; the base style comes first as the foundation, the current style and `static/diy/` override after it
|
|
1023
|
+
- `matchCssFiles(pattern, theme, style)` - Match style files by wildcard and return an array of full paths; `pattern` supports `*.css`, `blue*`, `*common*`, and defaults to `*.css`
|
|
1024
|
+
- `getCssFile(name, theme, style)` - Find style files by directory priority; returns an array when `name` contains `*` (empty array when nothing matches), otherwise a single path or `null`. `theme` / `style` default to the current values, so styles of another theme or style can be referenced
|
|
1025
|
+
- `getJsFiles()` - Get the script files as an array of full paths in ascending priority order, keeping only the highest-priority copy per filename; the application level comes first, the theme level and `static/diy/` after it
|
|
1026
|
+
- `matchJsFiles(pattern, theme)` - Match script files by wildcard and return an array of full paths; `pattern` supports `*.js`, `app*`, `*promo*`, and defaults to `*.js`; an empty array when nothing matches
|
|
1027
|
+
- `getJsFile(name, theme)` - Find script files by directory priority, returning the full path; returns an array when `name` contains `*`, otherwise a single path or `null` when absent
|
|
1028
|
+
- `set(obj)` - Set view data
|
|
1029
|
+
- `get(key)` - Get view data
|
|
1030
|
+
- `del(keys)` - Delete view data
|
|
1031
|
+
- `clearCache(path, file)` - Clear cache; `path` is relative to `cache_root`, omit it to clear only the dimension directory of the current instance, pass `'./'` to clear everything
|
|
1032
|
+
- `getCacheFile(options)` - Get the layered cache file path; the argument may be a config object or a cache filename string, and `options.style` / `options.lang` make it compute for that combination
|
|
1033
|
+
- `runFunc(name, req, ...args)` - Execute function hooks; returns a Promise. Both global and instance-level hooks run, with the instance-level return value taking precedence
|
|
1034
|
+
- `runHook(name, req, ...args)` - Execute function hooks (alias of runFunc)
|
|
1035
|
+
- `escape(body, restore)` - Escape/restore characters
|
|
1036
|
+
- `setFunc(name, func)` - Register a template function injected into the model on every render for templates to call; built-in functions and fields of the same name in this call's `model` take precedence
|
|
1037
|
+
- `removeFunc(name)` - Delete a registered template function, returns whether it existed and was removed
|
|
1038
|
+
- `getFuncs()` - Get the template function table
|
|
1039
|
+
|
|
1040
|
+
### Tpl Multi-user Methods
|
|
1041
|
+
|
|
1042
|
+
- `fork(config)` - Derive an instance; the user-level parameters are `{style, lang}`, while deployment-level settings (theme, directories, language packs, etc.) are inherited from the master instance; when only style and language are needed, pass them in the `view` options instead of deriving an instance
|
|
1043
|
+
- `getTheme()` - Get the current theme
|
|
1044
|
+
- `setTheme(theme, force)` - Set the current theme (a deployment-level setting users should not switch at runtime); keeps the old value and logs a warning when the directory is missing, `force: true` skips validation; also warns when `cache_dims` excludes `theme`
|
|
1045
|
+
- `getStyle()` - Get the current style, returns an empty string when unset
|
|
1046
|
+
- `setStyle(style, force)` - Set the current style (user-selectable); an empty string means no style is set (lookup and cache fall back to the default style); keeps the old value and logs a warning when the directory is missing, `force: true` skips validation
|
|
1047
|
+
- `getThemeDir(theme)` - Get the full path of a theme directory
|
|
1048
|
+
- `getStyleDir(theme, style)` - Get the full path of a style directory under a theme; when `style` is empty the style directory of that theme is returned
|
|
1049
1049
|
- `getTemplateDirs(theme)` - Get the template directory list (full paths) in priority order: `static/diy/` → the current theme → the default theme; `theme` defaults to the current value. Templates live directly under the theme directory organised by feature, and are not layered by style
|
|
1050
1050
|
- `getComponentDirs(theme)` - Get the component directory list (full paths) in priority order: `static/diy/component/`, the current theme's `component/`, the default theme's `component/`; isomorphic to the template chain with one extra fixed `component/` prefix on every level; `theme` defaults to the current value, and an empty `component_dir` yields an empty array
|
|
1051
|
-
- `getStyleDirs(theme, style)` - Get the style directory list (full paths) in priority order; styles are layered by theme and by style, so this covers **styles only** (`static/template/{theme}/style/{style}/`, including the style and `base` levels) and is not used for templates; `theme` / `style` default to the current values
|
|
1052
|
-
- `getJsDirs(theme)` - Get the script directory list (full paths) in descending priority: `static/diy/`, the current theme, the default theme, the application level
|
|
1053
|
-
- `getLang()` - Get the current language code
|
|
1054
|
-
- `setLang(lang)` - Set the current language, returns the applied language code
|
|
1055
|
-
- `t(key, ...args)` - Get translated text, equivalent to `$t` in templates, supports `a.b` nested keys; a single object argument fills `{name}` placeholders, otherwise `{0}`, `{1}` ... are replaced in order
|
|
1056
|
-
- `tc(key, count, ...args)` - Get plural translated text, `{count}` is the count placeholder
|
|
1057
|
-
- `getLangs()` - Get the supported language list
|
|
1058
|
-
- `reloadLangs(force)` - Reload language packs, keeping the current language; returns the language list. Uses the file fingerprint by default, `force: true` re-reads the files
|
|
1051
|
+
- `getStyleDirs(theme, style)` - Get the style directory list (full paths) in priority order; styles are layered by theme and by style, so this covers **styles only** (`static/template/{theme}/style/{style}/`, including the style and `base` levels) and is not used for templates; `theme` / `style` default to the current values
|
|
1052
|
+
- `getJsDirs(theme)` - Get the script directory list (full paths) in descending priority: `static/diy/`, the current theme, the default theme, the application level
|
|
1053
|
+
- `getLang()` - Get the current language code
|
|
1054
|
+
- `setLang(lang)` - Set the current language, returns the applied language code
|
|
1055
|
+
- `t(key, ...args)` - Get translated text, equivalent to `$t` in templates, supports `a.b` nested keys; a single object argument fills `{name}` placeholders, otherwise `{0}`, `{1}` ... are replaced in order
|
|
1056
|
+
- `tc(key, count, ...args)` - Get plural translated text, `{count}` is the count placeholder
|
|
1057
|
+
- `getLangs()` - Get the supported language list
|
|
1058
|
+
- `reloadLangs(force)` - Reload language packs, keeping the current language; returns the language list. Uses the file fingerprint by default, `force: true` re-reads the files
|
|
1059
1059
|
- `loadLang(lang)` - Reload and return the entries of one language, `null` when absent
|
|
1060
1060
|
- `setUser(user_id, prefs)` - Write user preferences; `prefs` is shaped like `{style, lang}` and only those two fields are stored; repeated writes for the same user merge by field and the saved preferences are returned
|
|
1061
1061
|
- `getUser(user_id)` - Get the preferences of one user, `null` when not registered
|
|
@@ -1063,117 +1063,116 @@ tpl.hook.addAction('header', function (m) { return '<header>instance-level</head
|
|
|
1063
1063
|
- `getUsers()` - Get the list of registered user ids
|
|
1064
1064
|
- `userOptions(user_id, options)` - Get render options for a user: a registered user yields the mapped `style` / `lang`, an unregistered user falls back to the style and language of the current instance; the result can be passed straight to `view` / `render`, and `options` may carry other render options
|
|
1065
1065
|
- `setStore(store)` - Set the user preference store; an empty or incomplete one falls back to the built-in in-memory `Memory`; returns the current instance
|
|
1066
|
-
- `getStore()` - Get the user preference store (`Memory` / `Sqlite` or a custom store object)
|
|
1067
|
-
|
|
1068
|
-
### Tpl Instance Properties
|
|
1069
|
-
|
|
1070
|
-
- `config` - Configuration object
|
|
1071
|
-
- `viewBag` - View data
|
|
1072
|
-
- `current_theme` - Current theme
|
|
1073
|
-
- `current_style` - Current style
|
|
1066
|
+
- `getStore()` - Get the user preference store (`Memory` / `Sqlite` or a custom store object)
|
|
1067
|
+
|
|
1068
|
+
### Tpl Instance Properties
|
|
1069
|
+
|
|
1070
|
+
- `config` - Configuration object
|
|
1071
|
+
- `viewBag` - View data
|
|
1072
|
+
- `current_theme` - Current theme
|
|
1073
|
+
- `current_style` - Current style
|
|
1074
1074
|
- `lang` - Current language instance (`Lang`); the language pack dictionary is shared, while the current language is independent
|
|
1075
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
|
|
1076
1076
|
- `funcs` - Instance-level template function table, written by `setFunc`
|
|
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
|
|
1078
|
-
- `dir` - Application root directory, i.e. `config.root`
|
|
1079
|
-
- `error` - Error message
|
|
1080
|
-
|
|
1081
|
-
### Configuration Options
|
|
1082
|
-
|
|
1083
|
-
- `root` - Application root directory, defaults to the current working directory; themes, language packs and the override directory are resolved relative to it
|
|
1084
|
-
- `theme_dir` - Theme directory, relative to the application root, defaults to `'./static/template/'`
|
|
1085
|
-
- `style_dir` - Style directory, relative to the theme directory, defaults to `'./style/'`
|
|
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
|
|
1087
|
-
- `diy_dir` - User override directory, relative to the application root, defaults to `'./static/diy/'`, highest priority
|
|
1088
|
-
- `lang_dir` - Application-level language pack directory, relative to the application root, filename is the language code, defaults to `'./static/lang/'`; empty disables file language packs
|
|
1089
|
-
- `theme_lang_dir` - Theme-level language pack directory, relative to the theme directory, defaults to `'./lang/'`, overriding the same keys of the application level
|
|
1090
|
-
- `js_dir` - Application-level script directory, relative to the application root, defaults to `'./static/js/'`, holding the shared scripts used by every theme
|
|
1091
|
-
- `theme_js_dir` - Theme-level script directory, relative to the theme directory, defaults to `'./js/'`, overriding same-named scripts of the application-level directory. Scripts are not layered by style (a style is a visual variant and scripts do not change with colour or lightness), so there are no matching style levels
|
|
1092
|
-
- `default_theme` - Default theme, defaults to `'default'`
|
|
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
|
|
1078
|
+
- `dir` - Application root directory, i.e. `config.root`
|
|
1079
|
+
- `error` - Error message
|
|
1080
|
+
|
|
1081
|
+
### Configuration Options
|
|
1082
|
+
|
|
1083
|
+
- `root` - Application root directory, defaults to the current working directory; themes, language packs and the override directory are resolved relative to it
|
|
1084
|
+
- `theme_dir` - Theme directory, relative to the application root, defaults to `'./static/template/'`
|
|
1085
|
+
- `style_dir` - Style directory, relative to the theme directory, defaults to `'./style/'`
|
|
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
|
|
1087
|
+
- `diy_dir` - User override directory, relative to the application root, defaults to `'./static/diy/'`, highest priority
|
|
1088
|
+
- `lang_dir` - Application-level language pack directory, relative to the application root, filename is the language code, defaults to `'./static/lang/'`; empty disables file language packs
|
|
1089
|
+
- `theme_lang_dir` - Theme-level language pack directory, relative to the theme directory, defaults to `'./lang/'`, overriding the same keys of the application level
|
|
1090
|
+
- `js_dir` - Application-level script directory, relative to the application root, defaults to `'./static/js/'`, holding the shared scripts used by every theme
|
|
1091
|
+
- `theme_js_dir` - Theme-level script directory, relative to the theme directory, defaults to `'./js/'`, overriding same-named scripts of the application-level directory. Scripts are not layered by style (a style is a visual variant and scripts do not change with colour or lightness), so there are no matching style levels
|
|
1092
|
+
- `default_theme` - Default theme, defaults to `'default'`
|
|
1093
1093
|
- `default_style` - Default style (a variant inside a theme), defaults to `'light'`; empty means no style layering
|
|
1094
|
-
- `base_style` - Base style directory name, defaults to `'base'`, acting as the shared stylesheet layer for all styles: it ranks below the current style and above the default theme, so shared styles placed there are reused by every style; the layer is skipped when it equals the current style, and an empty value disables it
|
|
1095
|
-
- `default_lang` - Default language code, defaults to `'zh_cn'`
|
|
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
|
|
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
|
|
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
|
|
1094
|
+
- `base_style` - Base style directory name, defaults to `'base'`, acting as the shared stylesheet layer for all styles: it ranks below the current style and above the default theme, so shared styles placed there are reused by every style; the layer is skipped when it equals the current style, and an empty value disables it
|
|
1095
|
+
- `default_lang` - Default language code, defaults to `'zh_cn'`
|
|
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
|
|
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
|
|
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
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
|
|
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
|
|
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
|
|
1102
|
-
- `cache_type` - Cache type, defaults to `'file'` (physical files); `'memory'` keeps the cache in process memory
|
|
1103
|
-
- `cache_max` - Max entries of the in-memory cache, defaults to `1000`; the earliest written entry is evicted beyond it, `0` means unlimited
|
|
1104
|
-
|
|
1105
|
-
### Hook Class Methods
|
|
1106
|
-
|
|
1107
|
-
- `addFunc(name, func, sort, alias)` - Add function hook
|
|
1108
|
-
- `addAction(name, func, sort, alias)` - Add action hook
|
|
1109
|
-
- `addFilter(name, func, sort, alias)` - Add filter hook
|
|
1110
|
-
- `runFunc(name, model, req, ...args)` - Execute function hook
|
|
1111
|
-
- `runAction(name, model, ...args)` - Execute action hook
|
|
1112
|
-
- `runFilter(name, model, ret, ...args)` - Execute filter hook
|
|
1113
|
-
- `delFunc(name, alias)` - Delete function hook
|
|
1114
|
-
- `delAction(name, alias)` - Delete action hook
|
|
1115
|
-
- `delFilter(name, alias)` - Delete filter hook
|
|
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
|
|
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
|
|
1102
|
+
- `cache_type` - Cache type, defaults to `'file'` (physical files); `'memory'` keeps the cache in process memory
|
|
1103
|
+
- `cache_max` - Max entries of the in-memory cache, defaults to `1000`; the earliest written entry is evicted beyond it, `0` means unlimited
|
|
1104
|
+
|
|
1105
|
+
### Hook Class Methods
|
|
1106
|
+
|
|
1107
|
+
- `addFunc(name, func, sort, alias)` - Add function hook
|
|
1108
|
+
- `addAction(name, func, sort, alias)` - Add action hook
|
|
1109
|
+
- `addFilter(name, func, sort, alias)` - Add filter hook
|
|
1110
|
+
- `runFunc(name, model, req, ...args)` - Execute function hook
|
|
1111
|
+
- `runAction(name, model, ...args)` - Execute action hook
|
|
1112
|
+
- `runFilter(name, model, ret, ...args)` - Execute filter hook
|
|
1113
|
+
- `delFunc(name, alias)` - Delete function hook
|
|
1114
|
+
- `delAction(name, alias)` - Delete action hook
|
|
1115
|
+
- `delFilter(name, alias)` - Delete filter hook
|
|
1116
1116
|
- `clear(name)` - Clear hooks
|
|
1117
1117
|
|
|
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.
|
|
1119
|
-
|
|
1120
|
-
## Notes
|
|
1121
|
-
|
|
1122
|
-
1. **Dependencies**:
|
|
1123
|
-
2. **Directory Permissions**: Ensure template directories have read/write permissions
|
|
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
|
|
1125
|
-
4. **Hook Priority**: Lower numerical values indicate higher priority and earlier execution
|
|
1126
|
-
5. **Async Processing**: Hook functions support async operations, pay attention to error handling
|
|
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
|
|
1128
|
-
7. **Character Escaping**: Automatic handling of template syntax characters to avoid conflicts
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
1134
|
-
13. **Theme and Style Validation**: `setTheme` / `setStyle` validate the directory by default, keeping the old value and logging a warning when it is missing; pass `force` to skip validation before the directory is created. An empty style string means no style is set, and lookup and cache fall back to the default style
|
|
1135
|
-
14. **Style and Theme Fallback**: `setStyle` only validates whether the style directory exists under the current or default theme, keeping the old value and logging a warning when it is missing; assigning `current_style` directly skips validation. Style lookup **only falls back across themes** (current theme + current style → current theme + base style → default theme + current style → default theme + base style) and **never across styles**, so files of other style directories are not inherited. Put shared styles in the base style directory (`base_style`, `base` by default) and keep only differences in each style directory; when one style needs its own version of a shared file, place a same-named file in that style directory to override the base style. This concerns stylesheets only: templates are not layered by style and always resolve along `static/diy/` → current theme → default theme
|
|
1136
|
-
15. **Translation Lookup**: Template files must call `$t('key')` to emit multilingual content; hard-coded text does not follow the language switch. Keep copy in JSON language packs under the language pack directories (the filename is the language code, e.g. `zh_cn.json`, `en_us.json`) and let templates only resolve entries
|
|
1137
|
-
16. **Language Cache Cost**: While `lang` stays in `cache_dims`, each language keeps its own copy of the cache for the same template. Drop the `lang` dimension if the templates are not actually multilingual
|
|
1138
|
-
17. **Language File Changes**: Language files are read only when an instance is created, and already created instances are not refreshed automatically; new instances and `fork` decide by file fingerprint whether to re-read. Call `reloadLangs()` or `loadLang(lang)` to apply changes to an existing instance. `setTheme` reloads automatically so theme language packs take effect
|
|
1139
|
-
18. **Language Pack Cache**: Language packs are cached in the static property `Tpl.lang_cache` keyed by "directory set + file fingerprint", so `fork` does not re-parse unchanged files. The cache is memory-only and is re-read after a process restart. The fingerprint relies on mtime, so a change within the same time granularity may go unnoticed — use `reloadLangs(true)` to force a refresh
|
|
1140
|
-
19. **Cache Filename**: `cache_filename` is always normalised to end with `cache_extname`, so `./index` and `./index.html` both become `index.cache.html`, which lets `clearCache` match by extension; when `view` receives `cache` without a `cache_filename`, the name is derived from the template path automatically
|
|
1141
|
-
20. **Automatic Invalidation**: The cache is rebuilt when the template file or a language pack file is newer than the cache, judged by modification time. Relying on timestamps means a change within the same time granularity may go unnoticed; language pack changes are not checked when `cache_dims` excludes `lang`
|
|
1142
|
-
21. **In-memory Cache**: With `cache_type: 'memory'` no physical files are produced and entries live in `Tpl.cache_store` with least-recently-used eviction. The cache is cleared on process restart, so in a multi-instance deployment each process keeps its own copy and they do not share invalidation state
|
|
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
|
|
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.
|
|
1119
|
+
|
|
1120
|
+
## Notes
|
|
1121
|
+
|
|
1122
|
+
1. **Dependencies**: `art-template` and `mm_lang` at runtime (the language instance comes from `mm_lang`'s `Lang`; this module no longer ships its own `BaseLang`); path and logging capabilities are built in, with no other external modules or global variables required
|
|
1123
|
+
2. **Directory Permissions**: Ensure template directories have read/write permissions
|
|
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
|
|
1125
|
+
4. **Hook Priority**: Lower numerical values indicate higher priority and earlier execution
|
|
1126
|
+
5. **Async Processing**: Hook functions support async operations, pay attention to error handling
|
|
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
|
|
1128
|
+
7. **Character Escaping**: Automatic handling of template syntax characters to avoid conflicts
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
1134
|
+
13. **Theme and Style Validation**: `setTheme` / `setStyle` validate the directory by default, keeping the old value and logging a warning when it is missing; pass `force` to skip validation before the directory is created. An empty style string means no style is set, and lookup and cache fall back to the default style
|
|
1135
|
+
14. **Style and Theme Fallback**: `setStyle` only validates whether the style directory exists under the current or default theme, keeping the old value and logging a warning when it is missing; assigning `current_style` directly skips validation. Style lookup **only falls back across themes** (current theme + current style → current theme + base style → default theme + current style → default theme + base style) and **never across styles**, so files of other style directories are not inherited. Put shared styles in the base style directory (`base_style`, `base` by default) and keep only differences in each style directory; when one style needs its own version of a shared file, place a same-named file in that style directory to override the base style. This concerns stylesheets only: templates are not layered by style and always resolve along `static/diy/` → current theme → default theme
|
|
1136
|
+
15. **Translation Lookup**: Template files must call `$t('key')` to emit multilingual content; hard-coded text does not follow the language switch. Keep copy in JSON language packs under the language pack directories (the filename is the language code, e.g. `zh_cn.json`, `en_us.json`) and let templates only resolve entries
|
|
1137
|
+
16. **Language Cache Cost**: While `lang` stays in `cache_dims`, each language keeps its own copy of the cache for the same template. Drop the `lang` dimension if the templates are not actually multilingual
|
|
1138
|
+
17. **Language File Changes**: Language files are read only when an instance is created, and already created instances are not refreshed automatically; new instances and `fork` decide by file fingerprint whether to re-read. Call `reloadLangs()` or `loadLang(lang)` to apply changes to an existing instance. `setTheme` reloads automatically so theme language packs take effect
|
|
1139
|
+
18. **Language Pack Cache**: Language packs are cached in the static property `Tpl.lang_cache` keyed by "directory set + file fingerprint", so `fork` does not re-parse unchanged files. The cache is memory-only and is re-read after a process restart. The fingerprint relies on mtime, so a change within the same time granularity may go unnoticed — use `reloadLangs(true)` to force a refresh
|
|
1140
|
+
19. **Cache Filename**: `cache_filename` is always normalised to end with `cache_extname`, so `./index` and `./index.html` both become `index.cache.html`, which lets `clearCache` match by extension; when `view` receives `cache` without a `cache_filename`, the name is derived from the template path automatically
|
|
1141
|
+
20. **Automatic Invalidation**: The cache is rebuilt when the template file or a language pack file is newer than the cache, judged by modification time. Relying on timestamps means a change within the same time granularity may go unnoticed; language pack changes are not checked when `cache_dims` excludes `lang`
|
|
1142
|
+
21. **In-memory Cache**: With `cache_type: 'memory'` no physical files are produced and entries live in `Tpl.cache_store` with least-recently-used eviction. The cache is cleared on process restart, so in a multi-instance deployment each process keeps its own copy and they do not share invalidation state
|
|
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
|
|
1144
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
|
|
1145
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
|
|
1146
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"
|
|
1147
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
|
|
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
|
|
1149
|
-
|
|
1150
|
-
## Development
|
|
1151
|
-
|
|
1152
|
-
Module layout:
|
|
1153
|
-
|
|
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
|
|
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`
|
|
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`)
|
|
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
|
|
1158
|
-
- `lib/css.js` - `Css`, style file collection (extends `Asset`)
|
|
1159
|
-
- `lib/js.js` - `Js`, script file collection (extends `Asset`)
|
|
1160
|
-
- `lib/lang.js` - `Lang`, language pack merging and caching
|
|
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
|
|
1149
|
+
|
|
1150
|
+
## Development
|
|
1151
|
+
|
|
1152
|
+
Module layout:
|
|
1153
|
+
|
|
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
|
|
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`
|
|
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`)
|
|
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
|
|
1158
|
+
- `lib/css.js` - `Css`, style file collection (extends `Asset`)
|
|
1159
|
+
- `lib/js.js` - `Js`, script file collection (extends `Asset`)
|
|
1160
|
+
- `lib/lang.js` - `Lang`, language pack merging and caching; the language instance (lookup, fallback, plural, formatting) comes from `mm_lang`'s `Lang`
|
|
1161
1161
|
- `lib/store.js` - `Memory`, the in-memory store: `user_id` → `{style, lang}`
|
|
1162
|
-
- `lib/sqlite.js` - `Sqlite`, the SQLite store: same contract as `Memory`, persisting to a file (relies on `node:sqlite`)
|
|
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/
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1162
|
+
- `lib/sqlite.js` - `Sqlite`, the SQLite store: same contract as `Memory`, persisting to a file (relies on `node:sqlite`)
|
|
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/hook.js`, `lib/tpl_rule.js` - hooks and template syntax rules
|
|
1167
|
+
|
|
1168
|
+
```bash
|
|
1169
|
+
# Run tests
|
|
1170
|
+
npm test
|
|
1171
|
+
|
|
1172
|
+
# Check code coverage
|
|
1173
|
+
npm run coverage
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
## License
|
|
1177
|
+
|
|
1179
1178
|
MIT License
|