mm_tpl 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +894 -52
- package/README_EN.md +905 -53
- package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +165 -0
- package/doc//350/256/276/350/256/241/346/200/273/347/273/223.md +381 -0
- package/index.js +28 -471
- package/lib/asset.js +133 -0
- package/lib/base.js +84 -0
- package/lib/base_lang.js +200 -0
- package/lib/css.js +34 -0
- package/lib/file.js +258 -0
- package/lib/hook.js +309 -0
- package/lib/js.js +34 -0
- package/lib/lang.js +188 -0
- package/lib/memory.js +101 -0
- package/lib/sqlite.js +174 -0
- package/lib/template.js +129 -0
- package/lib/tpl.js +1279 -0
- package/lib/tpl_rule.js +288 -0
- package/lib/util.js +196 -0
- package/package.json +18 -4
- package/hook.js +0 -302
- package/tpl_rule.js +0 -287
package/README_EN.md
CHANGED
|
@@ -19,10 +19,15 @@ Super Meimei Template Engine Module, based on art-template engine, provides powe
|
|
|
19
19
|
|
|
20
20
|
- 🚀 High-performance template engine based on art-template
|
|
21
21
|
- 🔧 Support for multiple template syntax rules
|
|
22
|
-
- 🎨
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
|
+
- 🔌 Powerful hook system with both global and instance-level hooks
|
|
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
|
|
26
31
|
- 🛡️ Comprehensive error handling mechanism
|
|
27
32
|
|
|
28
33
|
## Installation
|
|
@@ -44,16 +49,57 @@ npm install mm_tpl
|
|
|
44
49
|
```javascript
|
|
45
50
|
const {Tpl} = require('mm_tpl');
|
|
46
51
|
|
|
52
|
+
// Directory layout under the application root (see doc/目录结构.md):
|
|
53
|
+
// app/shop/
|
|
54
|
+
// config.js # { defaults: { theme, style, lang } }
|
|
55
|
+
// static/ # static resource root: templates, styles, scripts, language packs and the override directory all live here
|
|
56
|
+
// diy/ # user override directory, highest priority, may hold templates, components and scripts
|
|
57
|
+
// component/ # components overriding every theme
|
|
58
|
+
// js/ # application-level scripts, shared by every theme
|
|
59
|
+
// lang/ # application-level language packs, filename is the language code
|
|
60
|
+
// template/{theme}/ # templates sit directly under the theme directory, organised by feature
|
|
61
|
+
// index.html # the home page template of the theme
|
|
62
|
+
// article/list.html # feature directory: one directory per feature, at any depth
|
|
63
|
+
// component/ # components: shared partials reusable by feature pages
|
|
64
|
+
// bar.html
|
|
65
|
+
// js/ # theme-level scripts, overriding same-named application-level scripts
|
|
66
|
+
// lang/ # theme-level language packs, overriding the application level
|
|
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
|
+
|
|
47
70
|
// Create template instance
|
|
48
71
|
const tpl = new Tpl({
|
|
49
|
-
root: process.cwd(),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
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
|
|
56
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
|
|
57
103
|
```
|
|
58
104
|
|
|
59
105
|
### 3. Basic Template Rendering
|
|
@@ -128,8 +174,8 @@ const eachResult = tpl.render(`
|
|
|
128
174
|
### 5. View Rendering
|
|
129
175
|
|
|
130
176
|
```javascript
|
|
131
|
-
// Set global
|
|
132
|
-
|
|
177
|
+
// Set global data
|
|
178
|
+
Tpl.global_bag.siteName = "My Website";
|
|
133
179
|
|
|
134
180
|
// Set view variables
|
|
135
181
|
tpl.set({
|
|
@@ -137,8 +183,9 @@ tpl.set({
|
|
|
137
183
|
keywords: "keywords"
|
|
138
184
|
});
|
|
139
185
|
|
|
140
|
-
// Render view file
|
|
141
|
-
|
|
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", {
|
|
142
189
|
user: { name: "John" }
|
|
143
190
|
});
|
|
144
191
|
// Output: Rendering result <div>Hello World</div>
|
|
@@ -150,28 +197,171 @@ const title = tpl.get("title");
|
|
|
150
197
|
tpl.del("keywords");
|
|
151
198
|
```
|
|
152
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
|
+
|
|
153
238
|
### 6. Caching Mechanism
|
|
154
239
|
|
|
155
240
|
```javascript
|
|
156
|
-
// Use cache for view rendering
|
|
157
|
-
const html = tpl.view("./
|
|
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", {
|
|
158
243
|
data: someData
|
|
159
244
|
}, {
|
|
160
|
-
cache: true
|
|
161
|
-
|
|
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
|
|
162
254
|
});
|
|
163
255
|
|
|
164
|
-
//
|
|
165
|
-
tpl.
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
});
|
|
168
354
|
```
|
|
169
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
|
+
|
|
170
360
|
### 7. Hook System
|
|
171
361
|
|
|
172
362
|
#### Add Function Hook
|
|
173
363
|
```javascript
|
|
174
|
-
|
|
364
|
+
Tpl.global_hook.addFunc('list', async function(viewBag, req, type) {
|
|
175
365
|
// Process list logic
|
|
176
366
|
var query = req.query;
|
|
177
367
|
if (query.user_id) {
|
|
@@ -183,14 +373,14 @@ $.hook.addFunc('list', async function(viewBag, req, type) {
|
|
|
183
373
|
|
|
184
374
|
#### Add Action Hook
|
|
185
375
|
```javascript
|
|
186
|
-
|
|
376
|
+
Tpl.global_hook.addAction('header', function(viewBag, param) {
|
|
187
377
|
return "<header>Website Header</header>";
|
|
188
378
|
}, 1, "headerAction");
|
|
189
379
|
```
|
|
190
380
|
|
|
191
381
|
#### Add Filter Hook
|
|
192
382
|
```javascript
|
|
193
|
-
|
|
383
|
+
Tpl.global_hook.addFilter('content', function(viewBag, content, param) {
|
|
194
384
|
return content.replace("old text", "new text");
|
|
195
385
|
}, 1, "contentFilter");
|
|
196
386
|
```
|
|
@@ -215,25 +405,316 @@ const html = tpl.render(`
|
|
|
215
405
|
#### Delete Hooks
|
|
216
406
|
```javascript
|
|
217
407
|
// Delete specific hook
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
408
|
+
Tpl.global_hook.delFunc('list', 'listFunc');
|
|
409
|
+
Tpl.global_hook.delAction('header', 'headerAction');
|
|
410
|
+
Tpl.global_hook.delFilter('content', 'contentFilter');
|
|
221
411
|
|
|
222
412
|
// Clear all hooks
|
|
223
|
-
|
|
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' });
|
|
224
455
|
```
|
|
225
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
|
+
static/lang/zh_cn.json ← application-level Chinese
|
|
525
|
+
static/lang/en_us.json ← application-level English
|
|
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
|
+
|
|
226
602
|
## Advanced Features
|
|
227
603
|
|
|
228
|
-
### 1. Theme and Directory Support
|
|
604
|
+
### 1. Theme, Style and Directory Support
|
|
229
605
|
```javascript
|
|
230
|
-
// Set current theme
|
|
231
|
-
tpl.
|
|
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', ...]
|
|
232
676
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
//
|
|
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
|
|
237
718
|
```
|
|
238
719
|
|
|
239
720
|
### 2. Template Rules
|
|
@@ -246,7 +727,7 @@ Built-in support for multiple template rules, including:
|
|
|
246
727
|
|
|
247
728
|
### 3. Error Handling
|
|
248
729
|
```javascript
|
|
249
|
-
const html = tpl.view("./
|
|
730
|
+
const html = tpl.view("./index.html", data);
|
|
250
731
|
if (tpl.error) {
|
|
251
732
|
console.error("Rendering error:", tpl.error);
|
|
252
733
|
}
|
|
@@ -263,13 +744,267 @@ const restored = tpl.escape(escaped, true);
|
|
|
263
744
|
// Output: <div>${content}</div>
|
|
264
745
|
```
|
|
265
746
|
|
|
266
|
-
### 5. Global
|
|
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
|
+
|
|
267
820
|
```javascript
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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);
|
|
272
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
|
+
#### User Preference Store
|
|
858
|
+
|
|
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`:
|
|
860
|
+
|
|
861
|
+
```javascript
|
|
862
|
+
const {Tpl, Memory, Sqlite} = require('mm_tpl');
|
|
863
|
+
|
|
864
|
+
const tpl = new Tpl({ user_store: new Memory() }); // in-memory store (default)
|
|
865
|
+
const tpl2 = new Tpl({ user_store: new Sqlite('tpl.db') }); // persisted to a SQLite file
|
|
866
|
+
|
|
867
|
+
// Write: repeated writes for the same user merge by field, and a numeric and a
|
|
868
|
+
// string form of the same user_id are treated as the same user
|
|
869
|
+
tpl.setUser(1001, { style: 'dark', lang: 'en_us' });
|
|
870
|
+
tpl.setUser(1001, { lang: 'zh_cn' }); // only the language changes, the style stays dark
|
|
871
|
+
|
|
872
|
+
tpl.getUser(1001); // { style: 'dark', lang: 'zh_cn' }, or null when not registered
|
|
873
|
+
tpl.getUsers(); // ['1001'], the registered user ids
|
|
874
|
+
tpl.removeUser(1001); // remove, returns whether it existed
|
|
875
|
+
|
|
876
|
+
// Render options: a registered user maps to its style and language,
|
|
877
|
+
// an unregistered user falls back to the style and language of the current instance
|
|
878
|
+
tpl.userOptions(1001); // { style: 'dark', lang: 'zh_cn' }
|
|
879
|
+
tpl.userOptions(9999); // { style: <current style>, lang: <current language> }
|
|
880
|
+
tpl.userOptions(1001, { cache: true }); // other render options can be merged in
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
A custom store can be injected as well; it only needs the six synchronous methods (the built-in `Memory` / `Sqlite` follow the same contract):
|
|
884
|
+
|
|
885
|
+
```javascript
|
|
886
|
+
const tpl3 = new Tpl({
|
|
887
|
+
user_store: {
|
|
888
|
+
data: {},
|
|
889
|
+
getUser(user_id) {
|
|
890
|
+
return this.data[user_id] || null;
|
|
891
|
+
},
|
|
892
|
+
setUser(user_id, prefs) {
|
|
893
|
+
this.data[user_id] = Object.assign({}, this.data[user_id], prefs);
|
|
894
|
+
return this.data[user_id];
|
|
895
|
+
},
|
|
896
|
+
delUser(user_id) {
|
|
897
|
+
delete this.data[user_id];
|
|
898
|
+
return true;
|
|
899
|
+
},
|
|
900
|
+
hasUser(user_id) {
|
|
901
|
+
return this.data[user_id] !== undefined;
|
|
902
|
+
},
|
|
903
|
+
listUsers() {
|
|
904
|
+
return Object.keys(this.data);
|
|
905
|
+
},
|
|
906
|
+
clear() {
|
|
907
|
+
this.data = {};
|
|
908
|
+
return this;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
tpl3.setUser(2001, { style: 'light' }); // write
|
|
914
|
+
tpl3.getStore(); // the store object
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
`setUser` must merge by field itself and `clear` should return the store for chaining; normalising `user_id` is up to the implementation, while the built-in stores stringify it so that `1001` and `'1001'` are the same user.
|
|
918
|
+
|
|
919
|
+
Conventions to keep in mind:
|
|
920
|
+
|
|
921
|
+
- Only the `style` and `lang` fields are stored; anything else is ignored. The store performs no authorisation, so the caller must ensure `user_id` is trustworthy
|
|
922
|
+
- Derived instances share the same store: preferences belong to users, not to an instance, so `fork` reads and writes the same data (`tpl.getStore()` returns the store object)
|
|
923
|
+
- Storage is injected as an interface and defaults to the in-process `Memory`. Pass an object implementing the six synchronous methods `getUser` / `setUser` / `delUser` / `hasUser` / `listUsers` / `clear` via the `user_store` option to replace it with a file, a database or a cache service, or use the built-in `Memory` / `Sqlite` directly; an incomplete interface logs a warning and falls back to the in-memory store
|
|
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
|
+
- `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
|
+
|
|
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>'; });
|
|
273
1008
|
```
|
|
274
1009
|
|
|
275
1010
|
## API Reference
|
|
@@ -277,17 +1012,95 @@ if (global.$ && !$.Tpl) {
|
|
|
277
1012
|
### Tpl Class Methods
|
|
278
1013
|
|
|
279
1014
|
- `constructor(config)` - Constructor, create template instance
|
|
280
|
-
- `setConfig(config)` - Set configuration parameters
|
|
281
|
-
- `render(body, model, options)` - Render template string
|
|
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
|
|
282
1017
|
- `compile(body, options)` - Compile template to function
|
|
283
|
-
- `view(file, model, options)` - Render template file
|
|
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
|
|
284
1028
|
- `set(obj)` - Set view data
|
|
285
1029
|
- `get(key)` - Get view data
|
|
286
1030
|
- `del(keys)` - Delete view data
|
|
287
|
-
- `clearCache(path, file)` - Clear cache
|
|
288
|
-
- `
|
|
289
|
-
- `
|
|
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)
|
|
290
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
|
+
- `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
|
+
- `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
|
|
1059
|
+
- `loadLang(lang)` - Reload and return the entries of one language, `null` when absent
|
|
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
|
+
- `getUser(user_id)` - Get the preferences of one user, `null` when not registered
|
|
1062
|
+
- `removeUser(user_id)` - Remove the preferences of one user, returns whether it existed
|
|
1063
|
+
- `getUsers()` - Get the list of registered user ids
|
|
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
|
+
- `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
|
|
1074
|
+
- `lang` - Current language instance (`Lang`); the language pack dictionary is shared, while the current language is independent
|
|
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
|
+
- `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'`
|
|
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
|
|
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
|
|
291
1104
|
|
|
292
1105
|
### Hook Class Methods
|
|
293
1106
|
|
|
@@ -300,20 +1113,59 @@ if (global.$ && !$.Tpl) {
|
|
|
300
1113
|
- `delFunc(name, alias)` - Delete function hook
|
|
301
1114
|
- `delAction(name, alias)` - Delete action hook
|
|
302
1115
|
- `delFilter(name, alias)` - Delete filter hook
|
|
303
|
-
- `clear(name)` - Clear hooks
|
|
1116
|
+
- `clear(name)` - Clear hooks
|
|
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.
|
|
304
1119
|
|
|
305
1120
|
## Notes
|
|
306
1121
|
|
|
307
|
-
1. **Dependencies**:
|
|
1122
|
+
1. **Dependencies**: Only `art-template` at runtime; path, language and logging capabilities are built in, with no other external modules or global variables required
|
|
308
1123
|
2. **Directory Permissions**: Ensure template directories have read/write permissions
|
|
309
|
-
3. **Cache Management**:
|
|
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
|
|
310
1125
|
4. **Hook Priority**: Lower numerical values indicate higher priority and earlier execution
|
|
311
1126
|
5. **Async Processing**: Hook functions support async operations, pay attention to error handling
|
|
312
|
-
6. **File Search**:
|
|
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
|
|
313
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
|
+
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
|
+
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
|
+
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
|
+
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
|
|
314
1149
|
|
|
315
1150
|
## Development
|
|
316
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
|
|
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/base_lang.js` - `BaseLang`, built-in language core: word lookup, plural rules, date/number formatting
|
|
1167
|
+
- `lib/hook.js`, `lib/tpl_rule.js` - hooks and template syntax rules
|
|
1168
|
+
|
|
317
1169
|
```bash
|
|
318
1170
|
# Run tests
|
|
319
1171
|
npm test
|