@flun/html-template 4.0.10

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.
Files changed (59) hide show
  1. package/.env +9 -0
  2. package/LICENSE +15 -0
  3. package/build.js +3 -0
  4. package/compile.js +349 -0
  5. package/copy-files.js +200 -0
  6. package/customize/account.js +726 -0
  7. package/customize/data.json +484 -0
  8. package/customize/functions.js +48 -0
  9. package/customize/hotReloadInjector.js +25 -0
  10. package/customize/routes.js +141 -0
  11. package/customize/users.json +44 -0
  12. package/customize/variables.js +70 -0
  13. package/dev-server.js +344 -0
  14. package/dev.js +4 -0
  15. package/f-CHANGELOG.md +4 -0
  16. package/f-README.md +485 -0
  17. package/index.d.ts +133 -0
  18. package/index.js +4 -0
  19. package/package.json +77 -0
  20. package/restoreDefaults.js +8 -0
  21. package/services/templateService.js +962 -0
  22. package/static/about.css +118 -0
  23. package/static/auth.js +27 -0
  24. package/static/constants.css +138 -0
  25. package/static/img/dark.png +0 -0
  26. package/static/img/favicon.ico +0 -0
  27. package/static/img/light.png +0 -0
  28. package/static/img/top.png +0 -0
  29. package/static/index.css +86 -0
  30. package/static/mouseOrTouch.js +156 -0
  31. package/static/public.css +288 -0
  32. package/static/script.css +318 -0
  33. package/static/script.js +392 -0
  34. package/static/styling.css +874 -0
  35. package/static/styling.js +933 -0
  36. package/static/themeImg.css +10 -0
  37. package/static/themeImg.js +19 -0
  38. package/static/themeModule.js +222 -0
  39. package/static/topImg.css +19 -0
  40. package/static/topImg.js +21 -0
  41. package/static/utils/browser13.js +270 -0
  42. package/static/utils/closebrackets.js +166 -0
  43. package/static/utils/css-lint.js +308 -0
  44. package/static/utils/custom-css-hint.js +876 -0
  45. package/static/utils/foldgutter.js +141 -0
  46. package/static/utils/match-highlighter.js +70 -0
  47. package/templates/about.html +236 -0
  48. package/templates/account/2fa.html +184 -0
  49. package/templates/account/forgot-password.html +226 -0
  50. package/templates/account/login.html +230 -0
  51. package/templates/account/profile.html +977 -0
  52. package/templates/account/register.html +224 -0
  53. package/templates/account/reset-password.html +205 -0
  54. package/templates/account/verify-email.html +163 -0
  55. package/templates/base.html +71 -0
  56. package/templates/footer-content.html +5 -0
  57. package/templates/index.html +140 -0
  58. package/templates/script.html +209 -0
  59. package/templates/test-include.html +11 -0
@@ -0,0 +1,962 @@
1
+ /**
2
+ * 模板引擎服务
3
+ *
4
+ * 功能区块(按代码顺序):
5
+ * 1. 常量(路径和正则)及工具函数:高频正则预编译,路径安全检查,基础字符串处理,入口文件识别处理
6
+ * 2. 模板区块处理工具:区块解析和清理(忽略嵌套标签)
7
+ * 3. 包含文件处理:文件包含与依赖追踪
8
+ * 4. 用户自定义功能系统:路由/函数/变量加载
9
+ * 5. 模板功能处理系统:变量动态替换、函数执行、条件判断和循环处理
10
+ * 6. 模板结构验证:标签完整性检查
11
+ * 7. 模板文件操作:路径获取
12
+ * 8. 模板渲染引擎核心:模板合成,文件验证,渲染
13
+ * 9. 模块功能导出
14
+ */
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import vm from 'vm';
18
+ import { pathToFileURL } from 'url';
19
+ // ==================== 1. 常量声明及工具函数====================
20
+ let isCompilationMode = false;
21
+ /**
22
+ * 所有路径常量,其他文件从此导入
23
+ * >查看定义:@see {@link fsPromises}、{@link CWD}、{@link templatesDir}、{@link templatesAbsDir}、{@link staticDir}、
24
+ * {@link customizeDir}、{@link accountDir}、{@link defaultPort}、{@link writtenFilesToIgnore}
25
+ */
26
+ const fsPromises = fs.promises, CWD = process.cwd(), templatesDir = 'templates', templatesAbsDir = path.join(CWD, templatesDir),
27
+ pRes = path.resolve, staticDir = 'static', customizeDir = 'customize', accountDir = 'account', defaultPort = 7296,
28
+ userFeatures = {}, writtenFilesToIgnore = [], includedFiles = new Set(),
29
+ // 预编译所有高频正则表达式
30
+ includeRegex = /(\"|')\[include\s+([^\]]+)\](\"|')|\[include\s+([\S\s]+?)\]/gi, quotedVarRegex = /`\s*{{(.*?)}}\s*`/g,
31
+ userFuncRegex = /\{\{\s*user:\s*([^\s()]+?)\s*\(([^)]*)\)\s*\}\}/g, templateTagRegex = /\[!([^\]]*?)\]|\[\~([^\]]*?)\]/g,
32
+ extendsRegex = /^\[\s*extends\s+([^\]]+?)\s*\][^\r\n]*(?:\r\n|\n|\r|$)/i,
33
+ IfRegex = /\{\{if\s+([^}]+)\}\}([\s\S]*?)((?:\{\{else\s+if[^}]*\}\}[\s\S]*?)*)(?:\{\{else\}\}([\s\S]*?))?\{\{endif\}\}/gi,
34
+ elseIfRegex = /\{\{else\s+if\s+([^}]*)\}\}([\s\S]*?)(?=\{\{(?:else\s+if|else|endif)\}\})/gi,
35
+ forRegex = /\{\{for\s+(\w+)\s+in\s+([^}]+)\}\}([\s\S]*?)(?:\{\{empty\}\}([\s\S]*?))?\{\{endfor\}\}/gi,
36
+ forKeyValueRegex = /\{\{for\s+(\w+)\s*,\s*(\w+)\s+in\s+([^}]+)\}\}([\s\S]*?)(?:\{\{empty\}\}([\s\S]*?))?\{\{endfor\}\}/gi,
37
+ objectPropertyRegex = /\{\{(\w+\.\w+(?:\.\w+)*)\}\}/g, expressionRegex = /\{\{([^}]+)\}\}/g,
38
+ breakRegex = /\{\{\s*break\s*\}\}/g, continueRegex = /\{\{\s*continue\s*\}\}/g, specialCharsRegex = /[.*+?^${}()|[\]\\]/g,
39
+
40
+ // 不安全常量
41
+ unsafeKeys = ['__proto__', 'constructor', 'prototype', 'then', 'toString', 'valueOf', 'Object', 'Function', 'Promise'],
42
+
43
+ /**
44
+ * 重置正则表达式的lastIndex属性
45
+ * @param {RegExp} regex - 需要重置的正则表达式
46
+ * @returns {RegExp} 重置后的正则表达式
47
+ */
48
+ _resetRegex = regex => {
49
+ regex.lastIndex = 0;
50
+ return regex;
51
+ },
52
+
53
+ /**
54
+ * 检查路径是否安全(防止目录遍历攻击)
55
+ * @param {string} requestedPath - 请求的路径
56
+ * @param {string} baseDir - 基础目录
57
+ * @returns {boolean} 路径是否安全
58
+ */
59
+ _isSafePath = (requestedPath, baseDir) => {
60
+ const resolvedPath = pRes(requestedPath);
61
+ return resolvedPath.startsWith(pRes(baseDir));
62
+ },
63
+
64
+ /**
65
+ * 转义字符串中的正则表达式特殊字符
66
+ * @param {string} string - 待转义字符串
67
+ * @returns {string} 转义后的安全字符串
68
+ */
69
+ _escapeRegExp = string => {
70
+ return string.replace(_resetRegex(specialCharsRegex), '\\$&');
71
+ },
72
+
73
+ /**
74
+ * 安全地将值转换为字符串,处理 null、undefined 和对象
75
+ * @param {any} value - 需要转换的值
76
+ * @returns {string} 转换后的字符串
77
+ */
78
+ _safeToString = value => {
79
+ if (value === null || value === undefined) return '';
80
+ if (typeof value === 'object') {
81
+ try {
82
+ return JSON.stringify(value);
83
+ } catch (error) {
84
+ console.warn('对象转字符串失败:', error.message);
85
+ return '';
86
+ }
87
+ }
88
+
89
+ return String(value);
90
+ };
91
+
92
+ /**
93
+ * 动态识别入口文件(优先级策略)
94
+ * 1. 查找包含\<!-- \@entry -->标记的文件
95
+ * 2. 按优先级列表匹配(index.html > main.html > home.html)
96
+ * 3. 返回首字母排序的第一个HTML文件
97
+ * >查看定义:@see {@link findEntryFile}
98
+ * @param {string} cachedPages - 缓存模板列表
99
+ * @returns {Promise<string>} 入口文件名
100
+ */
101
+ const findEntryFile = async cachedPages => {
102
+ // 查找显式标记
103
+ for (const file of cachedPages) {
104
+ const content = await fsPromises.readFile(path.join(templatesAbsDir, file), 'utf8');
105
+ if (content.includes('<!-- @entry -->')) return file;
106
+ }
107
+
108
+ const priorityList = ['index.html', 'main.html', 'home.html']; // 优先级列表
109
+ for (const entry of priorityList) if (cachedPages.includes(entry)) return entry;
110
+
111
+ return cachedPages.sort()[0]; // 否则按首字母排序(保底)
112
+ },
113
+
114
+ // ==================== 2. 模板区块处理工具 ====================
115
+ /**
116
+ * 解析模板内容并定位所有区块的起止位置(忽略嵌套标签)
117
+ * @param {string} content - 模板内容
118
+ * @returns {Object} 区块元数据 {blockName: [{startIndex, endIndex, innerContent}]}
119
+ */
120
+ _findBlockPositions = content => {
121
+ const blocks = {};
122
+ let index = 0;
123
+
124
+ while (index < content.length) {
125
+ // 查找开标签
126
+ const startIndex = content.indexOf('[!', index), openEndIndex = content.indexOf(']', startIndex);
127
+ if (startIndex === -1 || openEndIndex === -1) break;
128
+
129
+ const openName = content.slice(startIndex + 2, openEndIndex).trim(), closeTag = `[~${openName}]`,
130
+ closeStartIndex = content.indexOf(closeTag, openEndIndex + 1), endIndex = closeStartIndex + closeTag.length;
131
+
132
+ // 没有找到闭标签,跳过
133
+ if (closeStartIndex === -1) {
134
+ index = openEndIndex + 1;
135
+ continue;
136
+ }
137
+
138
+ // 直接记录当前区块,不检查嵌套情况
139
+ if (!blocks[openName]) blocks[openName] = [];
140
+ blocks[openName].push({ startIndex, endIndex, innerContent: content.slice(openEndIndex + 1, closeStartIndex) });
141
+ index = endIndex;
142
+ }
143
+
144
+ return blocks;
145
+ },
146
+
147
+ /**
148
+ * 清除模板中的所有区块标签残留
149
+ * @param {string} html - 模板内容
150
+ * @returns {string} 清理后的纯净HTML
151
+ */
152
+ _cleanTemplateTags = html => {
153
+ return html.replace(_resetRegex(templateTagRegex), '');
154
+ };
155
+
156
+ // ==================== 3. 包含文件处理 ====================
157
+ /**
158
+ * 设置编译模式并重置文件依赖记录
159
+ * >查看定义:@see {@link setCompilationMode}
160
+ * @param {boolean} mode - 是否为编译模式
161
+ */
162
+ const setCompilationMode = mode => {
163
+ isCompilationMode = mode;
164
+ if (mode) includedFiles.clear();
165
+ };
166
+
167
+ /**
168
+ * 获取所有被包含的模板文件路径
169
+ * >查看定义:@see {@link getIncludedFiles}
170
+ * @returns {Set<string>} 包含文件路径集合
171
+ */
172
+ const getIncludedFiles = () => {
173
+ return new Set(includedFiles);
174
+ };
175
+
176
+ /**
177
+ * 递归处理模板中的包含指令
178
+ * >查看定义:@see {@link processIncludes}
179
+ * @param {string} content - 模板内容
180
+ * @param {string} [currentFile=''] - 当前处理文件路径
181
+ * @param {Set<string>} [inclusionStack] - 包含栈,用于检测循环包含
182
+ * @returns {Promise<string>} 处理后的内容
183
+ */
184
+ const processIncludes = async (content, currentFile = '', inclusionStack = new Set()) => {
185
+ const matches = [];
186
+ // 收集所有匹配项
187
+ for (const match of content.matchAll(includeRegex)) {
188
+ if (match[1] && match[3]) continue; // 跳过带引号的包含指令
189
+ matches.push({ fullMatch: match[0], fileName: (match[4] || match[2]).trim(), index: match.index });
190
+ }
191
+
192
+ if (matches.length === 0) return content; // 无匹配时直接返回
193
+ const sortedMatches = matches.sort((a, b) => b.index - a.index), parts = []; // 按索引降序排序
194
+ let lastIndex = content.length;
195
+
196
+ for (const { fullMatch, fileName, index } of sortedMatches) {
197
+ parts.push(content.slice(index + fullMatch.length, lastIndex)), lastIndex = index; // 添加当前匹配后的内容片段
198
+
199
+ let includePath;
200
+ if (path.isAbsolute(fileName)) includePath = path.join(templatesAbsDir, fileName);
201
+ else {
202
+ const currentDir = currentFile ? path.dirname(path.join(templatesAbsDir, currentFile)) : templatesAbsDir;
203
+ includePath = pRes(currentDir, fileName);
204
+ }
205
+
206
+ // 确保路径安全
207
+ if (!_isSafePath(includePath, templatesAbsDir)) {
208
+ console.warn(`⛔ 包含路径不安全,已跳过: ${fileName}`), parts.push('');
209
+ continue;
210
+ }
211
+
212
+ const relativeIncludePath = path.relative(templatesAbsDir, includePath); // 获取相对于模板目录的路径用于记录
213
+ // 检查循环包含
214
+ if (inclusionStack.has(includePath)) {
215
+ console.warn(`⚠️ 循环包含跳过: ${fileName}`), parts.push('');
216
+ continue;
217
+ }
218
+ // 检查自包含
219
+ else if (relativeIncludePath === currentFile) {
220
+ console.warn(`⚠️ 自包含跳过: ${fileName}`), parts.push('');
221
+ continue;
222
+ }
223
+
224
+ try {
225
+ let includedContent = await fsPromises.readFile(includePath, 'utf8');
226
+
227
+ if (isCompilationMode) includedFiles.add(relativeIncludePath); // 编译模式记录依赖
228
+ // 递归处理嵌套包含
229
+ const newStack = new Set(inclusionStack).add(includePath);
230
+ includedContent = await processIncludes(includedContent, relativeIncludePath, newStack), parts.push(includedContent);
231
+ } catch (error) {
232
+ console.warn(`⛔ 包含失败: ${fileName}`, error.message), parts.push('');
233
+ }
234
+ }
235
+
236
+ parts.push(content.slice(0, lastIndex)); // 添加首部内容片段
237
+ return parts.reverse().join(''); // 反转并拼接所有片段
238
+ }
239
+
240
+ // ==================== 4. 用户自定义功能系统 ====================
241
+ /**
242
+ * 运行时监控所有文件写入操作
243
+ * >查看定义:@see {@link monitorFileWrites}
244
+ * @returns {Function} 停止监控的函数
245
+ */
246
+ const monitorFileWrites = () => {
247
+ const sWrite = fs.writeFileSync, fWrite = fs.writeFile, pWrite = fsPromises.writeFile, pNormalize = path.normalize,
248
+ ignoreTimers = new Map(),
249
+
250
+ // 内联记录逻辑(加入忽略列表 + 2 秒后自动移除)
251
+ track = path => {
252
+ if (typeof path !== 'string') return;
253
+ const normalized = pNormalize(pRes(CWD, path));
254
+
255
+ if (ignoreTimers.has(normalized)) clearTimeout(ignoreTimers.get(normalized));
256
+ if (!writtenFilesToIgnore.includes(normalized)) writtenFilesToIgnore.push(normalized);
257
+ const timer = setTimeout(() => {
258
+ const idx = writtenFilesToIgnore.indexOf(normalized);
259
+ if (idx !== -1) writtenFilesToIgnore.splice(idx, 1);
260
+ ignoreTimers.delete(normalized);
261
+ }, 2000);
262
+
263
+ ignoreTimers.set(normalized, timer);
264
+ };
265
+
266
+ // 劫持同步写入
267
+ fs.writeFileSync = (file, ...args) => {
268
+ const r = sWrite.call(fs, file, ...args);
269
+ process.nextTick(track, file);
270
+ return r;
271
+ };
272
+
273
+ // 劫持异步写入(回调版)
274
+ fs.writeFile = (file, ...args) => {
275
+ const r = fWrite.call(fs, file, ...args);
276
+ process.nextTick(track, file);
277
+ return r;
278
+ };
279
+
280
+ // 劫持 Promise 版写入
281
+ fsPromises.writeFile = (file, ...args) => {
282
+ const r = pWrite.call(fsPromises, file, ...args);
283
+ process.nextTick(track, file);
284
+ return r;
285
+ };
286
+
287
+ // 停止监控
288
+ return () => {
289
+ fs.writeFileSync = sWrite, fs.writeFile = fWrite, fsPromises.writeFile = pWrite;
290
+ for (const timer of ignoreTimers.values()) clearTimeout(timer);
291
+ ignoreTimers.clear(), writtenFilesToIgnore.length = 0;
292
+ };
293
+ },
294
+
295
+ /**
296
+ * 异步安全加载模块(兼容 ESM 和 CJS)
297
+ * 优先使用 default 导出(ESM 默认导出 / CJS module.exports)
298
+ * 仅当 default 导出存在且包含 setupRoutes / functions / variables 时才使用,否则使用整个模块
299
+ * @param {string} modulePath - 模块文件路径
300
+ * @param {boolean} forceReload - 是否强制重新加载(绕过缓存)
301
+ */
302
+ _safeImport = async (modulePath, forceReload = false) => {
303
+ try {
304
+ let url = pathToFileURL(modulePath).href;
305
+ if (forceReload) url += `?t=${Date.now()}`;
306
+ const mod = await import(url), { default: d } = mod, hasUserFeature =
307
+ typeof d?.setupRoutes === 'function' || typeof d?.functions === 'object' || typeof d?.variables === 'object';
308
+
309
+ if (d && typeof d === 'object' && hasUserFeature) return d;
310
+ return mod;
311
+ } catch (error) {
312
+ console.error(`加载模块失败: ${modulePath}`, error.message);
313
+ return null;
314
+ }
315
+ };
316
+
317
+ /**
318
+ * 加载用户自定义功能(路由/函数/变量)
319
+ * >查看定义:@see {@link loadUserFeatures}
320
+ * @param {Object} app - Express应用实例(仅服务器模式需要)
321
+ * @param {boolean} isCompileMode - 是否为编译模式
322
+ * @param {boolean} forceReload - 是否强制重新加载(绕过缓存)
323
+ * @returns {Promise<Object>} 用户功能集合
324
+ */
325
+ const loadUserFeatures = async (app = null, isCompileMode = false, forceReload = false) => {
326
+ const featuresDir = path.join(CWD, customizeDir);
327
+
328
+ // 检查并创建目录(如果是编译模式)
329
+ try {
330
+ await fsPromises.access(featuresDir);
331
+ } catch {
332
+ if (isCompileMode) {
333
+ await fsPromises.mkdir(featuresDir, { recursive: true });
334
+ console.log(`📁 已创建用户功能目录: ${customizeDir}`);
335
+ }
336
+ }
337
+
338
+ userFeatures.variables = {}, userFeatures.functions = {};
339
+ try {
340
+ const files = await fsPromises.readdir(featuresDir), jsFiles = files.filter(file => file.endsWith('.js'));
341
+ console.log(`🔧 正在加载 (${jsFiles.length}个用户自定义功能文件):`);
342
+
343
+ for (const file of jsFiles) {
344
+ const featurePath = path.join(featuresDir, file), userFeature = await _safeImport(featurePath, forceReload);
345
+
346
+ if (!userFeature) {
347
+ console.log(` ❌ ${file} - 加载失败`);
348
+ continue;
349
+ }
350
+
351
+ // 注册路由(仅限服务器模式且有 app 实例)
352
+ if (typeof userFeature.setupRoutes === 'function' && app) userFeature.setupRoutes(app);
353
+
354
+ // 收集函数
355
+ if (userFeature.functions && typeof userFeature.functions === 'object') {
356
+ Object.keys(userFeature.functions).forEach(funcName => {
357
+ if (typeof userFeature.functions[funcName] === 'function')
358
+ userFeatures.functions[`${file.replace('.js', '')}.${funcName}`] = userFeature.functions[funcName];
359
+ });
360
+ }
361
+
362
+ // 收集变量
363
+ if (userFeature.variables && typeof userFeature.variables === 'object')
364
+ Object.assign(userFeatures.variables, userFeature.variables);
365
+
366
+ console.log(` ✅ ${file} - 加载成功`);
367
+ }
368
+ console.log('✅ 所有用户功能加载完成');
369
+ } catch (error) {
370
+ console.error('读取用户功能目录失败:', error.message);
371
+ }
372
+
373
+ return userFeatures;
374
+ },
375
+
376
+ // ==================== 5. 模板功能处理系统 ====================
377
+ /**
378
+ * 检查模板内容中是否还有未处理的标签
379
+ * @param {string} content - 模板内容
380
+ * @returns {boolean} 是否存在未处理的标签
381
+ */
382
+ _hasUnprocessedTags = content => {
383
+ const tagsToCheck = [forRegex, forKeyValueRegex, IfRegex, userFuncRegex, expressionRegex, objectPropertyRegex];
384
+
385
+ return tagsToCheck.some(regex => {
386
+ _resetRegex(regex);
387
+ return regex.test(content);
388
+ });
389
+ },
390
+
391
+ /**
392
+ * 处理所有循环结构,包括简单循环和键值对循环
393
+ * 支持特性:
394
+ * - 简单循环: {{for item in collection}}...{{empty}}...{{endfor}}
395
+ * - 键值对循环: {{for key, value in object}}...{{empty}}...{{endfor}}
396
+ * - empty分支: 当集合为空时显示替代内容
397
+ * - 自动循环变量: item_index, item_isFirst, item_isLast
398
+ * - 循环控制: 支持{{break}}和{{continue}}语句
399
+ * @param {string} content - 模板内容
400
+ * @param {Object} variables - 变量上下文
401
+ * @returns {string} 处理后的内容,所有循环结构已展开为实际内容
402
+ */
403
+ _processLoops = (content, variables) => {
404
+ let result = content;
405
+
406
+ // 循环处理器
407
+ const processLoop = (itemNames, collectionName, loopContent, emptyContent, variables) => {
408
+ const namesArray = Array.isArray(itemNames) ? itemNames : [itemNames];
409
+
410
+ if (namesArray.some(name => unsafeKeys.includes(name))) {
411
+ console.warn(`检测到不安全的循环变量名: ${namesArray.join(', ')}`);
412
+ return '';
413
+ }
414
+
415
+ try {
416
+ const collection = _evaluateExpression(collectionName, variables),
417
+ isEmpty = !collection || (Array.isArray(collection) && collection.length === 0) ||
418
+ (typeof collection === 'object' && Object.keys(collection).length === 0);
419
+
420
+ if (isEmpty) return emptyContent ? processVariables(emptyContent, variables) : '';
421
+ let loopResult = '';
422
+ const primaryVar = namesArray[0], isKeyValue = namesArray.length > 1,
423
+ entries = isKeyValue ? Object.entries(collection) : collection.map((v, i) => [i, v]);
424
+
425
+ for (let i = 0; i < entries.length; i++) {
426
+ const [key, value] = entries[i],
427
+ loopVariables = {
428
+ ...variables, [primaryVar]: isKeyValue ? key : value, [`${primaryVar}_index`]: i,
429
+ [`${primaryVar}_isFirst`]: i === 0, [`${primaryVar}_isLast`]: i === entries.length - 1
430
+ };
431
+
432
+ if (isKeyValue) loopVariables[namesArray[1]] = value;
433
+ let processedContent = processVariables(loopContent, loopVariables);
434
+ // 检查并处理循环控制标签
435
+ if (processedContent.includes('{{break}}')) {
436
+ processedContent = processedContent.replace(_resetRegex(breakRegex), '');
437
+ break;
438
+ } else if (processedContent.includes('{{continue}}')) {
439
+ processedContent = processedContent.replace(_resetRegex(continueRegex), '');
440
+ continue;
441
+ }
442
+
443
+ loopResult += processedContent;
444
+ }
445
+
446
+ return loopResult;
447
+ } catch (error) {
448
+ console.error(`处理循环时出错: ${collectionName}`, error.message);
449
+ return '';
450
+ }
451
+ }
452
+
453
+ // 处理键值对循环
454
+ result = result.replace(_resetRegex(forKeyValueRegex), (_match, keyName, valueName, collectionName, loopContent,
455
+ emptyContent) => processLoop([keyName, valueName], collectionName, loopContent, emptyContent, variables));
456
+
457
+ // 处理简单循环
458
+ result = result.replace(_resetRegex(forRegex), (_match, itemName, collectionName, loopContent, emptyContent) =>
459
+ processLoop(itemName, collectionName, loopContent, emptyContent, variables));
460
+
461
+ return result;
462
+ },
463
+
464
+ /**
465
+ * 处理条件判断结构,支持多分支条件判断
466
+ * 语法支持:
467
+ * - 主条件: {{if condition}}...{{endif}}
468
+ * - 多分支: {{if condition}}...{{else if condition2}}...{{else}}...{{endif}}
469
+ * - 嵌套条件: 支持最多20层嵌套,通过迭代方式处理
470
+ * 使用COMPLEX_IF_REGEX匹配主结构,ELSE_IF_REGEX匹配else if分支
471
+ * @param {string} content - 模板内容
472
+ * @param {Object} variables - 变量上下文
473
+ * @returns {string} 处理后的内容,条件判断已根据变量值解析
474
+ */
475
+ _processConditionals = (content, variables) => {
476
+ let result = content, hasChanges = true, iterationCount = 0;
477
+ const maxIterations = 20;
478
+
479
+ while (hasChanges && iterationCount < maxIterations) {
480
+ iterationCount++, hasChanges = false;
481
+ // 使用新的正则表达式来匹配多分支条件
482
+ result = result.replace(_resetRegex(IfRegex), (_match, ifCondition, ifContent, elseIfBlocks, elseContent) => {
483
+ try {
484
+ const allContent = ifContent + (elseIfBlocks || '') + (elseContent || '');
485
+ if (allContent.includes('{{if')) hasChanges = true; // 检查是否有嵌套条件
486
+ if (_evaluateExpression(ifCondition, variables)) return ifContent; // 检查主条件
487
+
488
+ // 处理 else if 分支
489
+ if (elseIfBlocks)
490
+ for (const elseIfMatch of elseIfBlocks.matchAll(elseIfRegex)) {
491
+ const elseIfCondition = elseIfMatch[1], elseIfContent = elseIfMatch[2];
492
+ if (_evaluateExpression(elseIfCondition, variables)) return elseIfContent;
493
+ }
494
+
495
+ return elseContent || ''; // 处理 else 分支
496
+ } catch (error) {
497
+ console.error(`处理条件判断时出错: ${ifCondition}`, error.message);
498
+ return '';
499
+ }
500
+ });
501
+ }
502
+
503
+ return result;
504
+ },
505
+
506
+ /**
507
+ * 执行用户自定义函数
508
+ * @param {string} funcName - 函数名称
509
+ * @param {...any} args - 函数参数
510
+ * @returns {any} 函数执行结果
511
+ */
512
+ _executeUserFunction = (funcName, ...args) => {
513
+ try {
514
+ if (!userFeatures.functions[funcName]) throw new Error(`找不到函数: ${funcName}`);
515
+ return userFeatures.functions[funcName](...args);
516
+ } catch (error) {
517
+ console.error(`执行用户函数 ${funcName} 时出错:`, error.message);
518
+ return null;
519
+ }
520
+ },
521
+
522
+ /**
523
+ * 处理用户自定义函数调用,支持多种参数类型和复杂表达式
524
+ * 函数调用格式: {{user:functionName(arg1, arg2, ...)}}
525
+ * 参数类型支持:
526
+ * - 字符串: "string" 或 'string'(引号内的内容作为字面量)
527
+ * - 变量: {{variable}}(变量引用,使用当前上下文中的值)
528
+ * - 布尔值: true/false(直接转换为布尔类型)
529
+ * - 数字: 123(直接转换为数字类型)
530
+ * - 特殊值: null/undefined(转换为对应JavaScript值)
531
+ * 函数从customize目录加载,支持模块化组织
532
+ * @param {string} content - 模板内容
533
+ * @param {Object} variables - 变量上下文
534
+ * @returns {string} 处理后的内容,函数调用已替换为执行结果
535
+ */
536
+ _processUserFunctions = (content, variables) => {
537
+ return content.replace(_resetRegex(userFuncRegex),
538
+ (_match, funcCall, argsStr) => {
539
+ try {
540
+ // 检查函数名安全性
541
+ if (unsafeKeys.includes(funcCall)) {
542
+ console.warn(`检测到不安全的函数名: ${funcCall}`);
543
+ return '';
544
+ }
545
+
546
+ const cleanedArgs = argsStr.split(',')
547
+ .map(arg => arg.trim()).filter(arg => arg !== '')
548
+ .map(arg => {
549
+ // 字符串处理
550
+ const quotedMatch = arg.match(/^["'](.*)["']$/);
551
+ if (quotedMatch) return quotedMatch[1];
552
+
553
+ // 变量处理 - 增强安全性检查
554
+ const varMatch = arg.match(/\{\{(\w+)\}\}/);
555
+ if (varMatch) {
556
+ const varName = varMatch[1];
557
+ if (unsafeKeys.includes(varName)) {
558
+ console.warn(`检测到不安全的变量名: ${varName}`);
559
+ return undefined;
560
+ }
561
+ if (variables[varName] !== undefined) return variables[varName];
562
+ }
563
+
564
+ // 特殊值和数字处理
565
+ if (arg === 'true') return true;
566
+ if (arg === 'false') return false;
567
+ if (arg === 'null') return null;
568
+ if (arg === 'undefined') return undefined;
569
+ if (!isNaN(Number(arg))) return Number(arg);
570
+
571
+ // 安全性检查
572
+ if (unsafeKeys.includes(arg)) {
573
+ console.warn(`检测到不安全的变量名: ${arg}`);
574
+ return undefined;
575
+ }
576
+ return variables[arg] !== undefined ? variables[arg] : arg;
577
+ }).filter(arg => arg !== undefined); // 过滤掉不安全的值
578
+
579
+ const funcResult = _executeUserFunction(funcCall, ...cleanedArgs);
580
+ return _safeToString(funcResult);
581
+ } catch (error) {
582
+ console.error(`处理用户函数调用 ${funcCall} 时出错:`, error.message);
583
+ return '';
584
+ }
585
+ });
586
+ },
587
+
588
+ /**
589
+ * 根据点分隔的路径从对象中获取嵌套属性值
590
+ * 支持多级属性访问,如: user.profile.name
591
+ * 使用reduce方法逐级访问属性,遇到undefined时停止并返回
592
+ * 安全处理不存在的路径,避免抛出异常
593
+ * @param {Object} obj - 源对象
594
+ * @param {string} path - 点分隔的属性路径
595
+ * @returns {any} 属性值,如果路径不存在则返回undefined
596
+ */
597
+ _getValueByPath = (obj, path) => {
598
+ return path.split('.').reduce((current, key) => {
599
+ if (current === null || current === undefined) return undefined;
600
+ if (unsafeKeys.includes(key)) return undefined; // 防止原型污染
601
+ return Object.hasOwnProperty.call(current, key) ? current[key] : undefined; // 检查属性是否存在
602
+ }, obj);
603
+ },
604
+
605
+ /**
606
+ * 创建安全的沙箱环境用于表达式求值,防止恶意代码执行
607
+ * 复制原始变量但阻止原型访问,确保安全性
608
+ * 添加安全的工具函数支持:
609
+ * - 基础类型: String, Number, Boolean, Array, Date, Math, JSON
610
+ * - 逻辑运算符: and, or, not, eq, neq, gt, lt, gte, lte
611
+ * 使用Object.create(null)创建无原型的干净对象
612
+ * @param {Object} variables - 原始变量上下文
613
+ * @returns {Object} 安全的沙箱环境,包含变量和受限函数
614
+ */
615
+ _createSafeSandbox = variables => {
616
+ const safeVariables = Object.create(null);
617
+
618
+ // 复制原始变量但阻止原型访问
619
+ for (const key in variables)
620
+ if (Object.hasOwnProperty.call(variables, key) && !unsafeKeys.includes(key)) safeVariables[key] = variables[key];
621
+
622
+ // 添加安全的工具函数
623
+ const safeFunctions = {
624
+ String, Number, Boolean, Array, Date, Math, JSON,
625
+ // 添加逻辑运算符支持
626
+ and: (a, b) => a && b, or: (a, b) => a || b, not: a => !a, eq: (a, b) => a === b, neq: (a, b) => a !== b,
627
+ gt: (a, b) => a > b, lt: (a, b) => a < b, gte: (a, b) => a >= b, lte: (a, b) => a <= b
628
+ };
629
+
630
+ return { ...safeVariables, ...safeFunctions };
631
+ },
632
+
633
+ /**
634
+ * 表达式求值函数,在安全沙箱环境中执行JavaScript表达式
635
+ * 使用vm模块创建安全上下文,避免使用eval
636
+ * 将沙箱环境中的变量和函数作为参数传入执行上下文
637
+ * 支持完整的JavaScript表达式语法,包括:
638
+ * - 算术运算: +, -, *, /, %
639
+ * - 比较运算: ==, !=, ===, !==, >, <, >=, <=
640
+ * - 逻辑运算: &&, ||, !
641
+ * - 条件运算: ?:
642
+ * - 函数调用: func(arg1, arg2)
643
+ * - 属性访问: obj.property
644
+ * @param {string} expr - 待求值的JavaScript表达式
645
+ * @param {Object} variables - 变量上下文
646
+ * @returns {any} 表达式求值结果,求值失败时返回null
647
+ */
648
+ _evaluateExpression = (expr, variables) => {
649
+ try {
650
+ // 创建安全沙箱
651
+ const context = vm.createContext({
652
+ ..._createSafeSandbox(variables), process: undefined, global: undefined, console: Object.create(null),
653
+ setTimeout: undefined, setInterval: undefined, setImmediate: undefined, Buffer: undefined, require: undefined
654
+ }),
655
+ result = vm.runInContext(`(${expr})`, context, { timeout: 1500, displayErrors: false }); // 执行表达式
656
+
657
+ return result;
658
+ } catch (error) {
659
+ console.error(`表达式求值失败: ${expr}`, error.message);
660
+ return null;
661
+ }
662
+ },
663
+
664
+ /**
665
+ * 处理对象属性访问、简单变量替换和复杂表达式求值
666
+ * 使用evaluateExpression函数在安全沙箱中执行表达式
667
+ * 功能包括:
668
+ * - 还原带反引号变量: `{{variable}}` → {{variable}}
669
+ * - 对象属性访问: {{object.property}}支持多级属性访问
670
+ * - 简单变量替换: {{variable}}替换为变量值
671
+ * - 跳过特殊标签: 用户函数,循环,控制和判断
672
+ * 表达式支持特性:
673
+ * - 数学运算: {{1 + 2 * 3}}
674
+ * - 逻辑运算: {{a && b || c}}
675
+ * - 比较运算: {{value > 10}}
676
+ * - 三元运算符: {{condition ? value1 : value2}}
677
+ * - 函数调用: {{Math.max(a, b)}}
678
+ * 使用OBJECT_PROPERTY_REGEX匹配对象属性访问模式
679
+ * 使用getValueByPath函数解析多级属性路径
680
+ * @param {string} content - 模板内容
681
+ * @param {Object} variables - 变量上下文
682
+ * @returns {string} 处理后的内容,简单变量和对象属性已替换
683
+ */
684
+ _processVariablesAndExpressions = (content, variables) => {
685
+ let result = content;
686
+ result = result.replace(_resetRegex(quotedVarRegex), (_match, inner) => `{{${inner}}}`); // 处理带反引号变量
687
+
688
+ // 处理对象属性访问
689
+ result = result.replace(_resetRegex(objectPropertyRegex), (_match, path) => {
690
+ try {
691
+ const value = _getValueByPath(variables, path);
692
+ return _safeToString(value);
693
+ } catch (error) {
694
+ console.warn(`处理对象属性访问 ${path} 时出错:`, error.message);
695
+ return '';
696
+ }
697
+ });
698
+
699
+ // 处理简单变量替换
700
+ Object.entries(variables).forEach(([key, value]) => {
701
+ if (unsafeKeys.includes(key)) return;
702
+ try {
703
+ const stringValue = _safeToString(value), regex = new RegExp(`{{\\s*${_escapeRegExp(key)}\\s*}}`, 'g');
704
+ result = result.replace(regex, stringValue);
705
+ } catch (error) {
706
+ console.warn(`处理变量 ${key} 时出错:`, error.message);
707
+ }
708
+ });
709
+
710
+ // 处理复杂表达式求值
711
+ result = result.replace(_resetRegex(expressionRegex), (match, expression) => {
712
+ if (!expression.trim()) return ''; // 跳过空表达式
713
+ if (expression.includes('user:')) return match; // 跳过已经被处理的用户函数调用
714
+
715
+ // 跳过条件判断标签、循环标签以及循环控制标签
716
+ const trimmedExpr = expression.trim();
717
+ if (trimmedExpr.startsWith('if ') || trimmedExpr === 'else' || trimmedExpr === 'endif' ||
718
+ trimmedExpr.startsWith('for ') || trimmedExpr === 'endfor' || trimmedExpr === 'empty' ||
719
+ trimmedExpr === 'break' || trimmedExpr === 'continue') return match;
720
+
721
+ try {
722
+ // 尝试直接求值表达式
723
+ const value = _evaluateExpression(expression.trim(), variables);
724
+ return _safeToString(value);
725
+ } catch (error) {
726
+ console.warn(`表达式求值失败: ${expression}`, error.message);
727
+ return '';
728
+ }
729
+ });
730
+
731
+ return result;
732
+ },
733
+
734
+ // 模块处理函数数组(所有依赖函数已定义,顺序安全)
735
+ processingPhases = [_processLoops, _processConditionals, _processUserFunctions, _processVariablesAndExpressions],
736
+
737
+ /**
738
+ * 迭代式模板处理器
739
+ * 按数组顺序多次处理模板,直到没有变化或达到最大迭代次数
740
+ * @param {string} content - 模板内容
741
+ * @param {Object} variables - 变量上下文
742
+ * @param {number} maxIterations - 最大迭代次数:10
743
+ * @returns {string} 处理后的内容
744
+ */
745
+ _processIteratively = (content, variables, maxIterations = 10) => {
746
+ let result = content, previousResult, iteration = 0;
747
+ do {
748
+ previousResult = result;
749
+ for (const handler of processingPhases) result = handler(result, variables); // 处理模板
750
+ iteration++;
751
+ } while (result !== previousResult && iteration < maxIterations && _hasUnprocessedTags(result));
752
+
753
+ if (iteration >= maxIterations) console.warn(`模板处理达到最大迭代次数(${maxIterations}),可能包含无限循环或复杂嵌套`);
754
+ return result;
755
+ };
756
+
757
+ /**
758
+ * 迭代式变量处理主入口函数;
759
+ * 多次扫描模板内容,直到没有未处理的标签或达到最大迭代次数
760
+ * >查看定义:@see {@link processVariables}
761
+ * @param {string} content - 待处理的模板内容
762
+ * @param {Object} requestVariables - 请求级变量,与用户变量合并后使用
763
+ * @returns {string} 处理后的内容,所有动态部分已被替换为实际值
764
+ */
765
+ const processVariables = (content, requestVariables = {}) => {
766
+ const allVariables = { ...userFeatures.variables, ...requestVariables };
767
+ return _processIteratively(content, allVariables); // 使用迭代式处理替代线性管道
768
+ },
769
+
770
+ // ==================== 6. 模板结构验证 ====================
771
+ /**
772
+ * 验证模板标签的完整性和嵌套结构
773
+ * @param {string} content - 模板内容
774
+ * @returns {Array} 结构错误信息集合
775
+ */
776
+ _validateTemplateStructure = (content) => {
777
+ const stack = [], errors = [];
778
+ let line = 1;
779
+
780
+ // 公共标签解析
781
+ const parseTag = (i, tagType, newLine) => {
782
+ const endIndex = content.indexOf(']', i);
783
+ if (endIndex === -1) {
784
+ errors.push(`第 ${newLine} 行: ${tagType}标签缺少闭合方括号`);
785
+ return { B: true };
786
+ }
787
+
788
+ const rawName = content.slice(i + 2, endIndex);
789
+ if (rawName.trim() === '') {
790
+ errors.push(`第 ${newLine} 行: 空${tagType}标签名称`);
791
+ return { C: true, endIndex };
792
+ }
793
+ const invalidChars = ['[', ']', '{', '}', '~'], foundInvalidChars = invalidChars.filter(c => rawName.includes(c));
794
+ if (foundInvalidChars.length > 0) {
795
+ errors.push(`第 ${newLine} 行: 标签名称包含非法字符 ${foundInvalidChars.join(',')}`);
796
+ return { C: true, endIndex };
797
+ }
798
+
799
+ return { name: rawName.trim(), endIndex };
800
+ }
801
+
802
+ // 重构后的主循环
803
+ for (let i = 0; i < content.length; i++) {
804
+ const char = content[i];
805
+ if (char === '\n') line++;
806
+
807
+ // 开标签检测
808
+ if (content.startsWith('[!', i)) {
809
+ const { B, C, endIndex, name } = parseTag(i, '开', line);
810
+ if (B) break;
811
+ if (C) {
812
+ i = endIndex;
813
+ continue;
814
+ }
815
+ stack.push({ name, line }), i = endIndex;
816
+ }
817
+ // 闭标签检测
818
+ else if (content.startsWith('[~', i)) {
819
+ const { B, C, endIndex, name } = parseTag(i, '闭', line);
820
+ if (B) break;
821
+ if (C) {
822
+ i = endIndex;
823
+ continue;
824
+ }
825
+
826
+ if (stack.length === 0) errors.push(`第 ${line} 行: 多余的闭标签 '${name}'`);
827
+ else {
828
+ const lastOpen = stack[stack.length - 1];
829
+ if (lastOpen.name === name) stack.pop();
830
+ else errors.push(`第 ${line} 行: 标签不匹配, 期望 '${lastOpen.name}' 但找到 '${name}'`);
831
+ }
832
+ i = endIndex;
833
+ }
834
+ }
835
+
836
+ stack.forEach(tag => { errors.push(`第 ${tag.line} 行: 未闭合的区块 '${tag.name}'`); });
837
+ return errors;
838
+ };
839
+
840
+ // ==================== 7. 模板文件操作 ====================
841
+ /**
842
+ * 获取模板目录下所有可用的HTML文件路径(排除base.html)
843
+ * >查看定义:@see {@link getAvailableTemplates}
844
+ * @returns {Promise<string[]>} 过滤后HTML文件路径数组
845
+ */
846
+ const getAvailableTemplates = async () => {
847
+ try {
848
+ const getAllHtmlFiles = async (dir) => {
849
+ const results = [], items = await fsPromises.readdir(dir);
850
+ for (const item of items) {
851
+ const fullPath = path.join(dir, item), stat = await fsPromises.stat(fullPath);
852
+ if (stat.isDirectory()) results.push(...(await getAllHtmlFiles(fullPath)));
853
+ else if (item !== 'base.html' && path.extname(item).toLowerCase() === '.html') {
854
+ const relativePath = path.relative(templatesAbsDir, fullPath);
855
+ results.push(relativePath.replaceAll('\\', '/'));
856
+ }
857
+ }
858
+ return results;
859
+ }, templates = await getAllHtmlFiles(templatesAbsDir);
860
+ if (templates.length === 0) throw new Error('未找到任何可用模板文件,请检查模板目录');
861
+
862
+ return templates;
863
+ } catch (error) {
864
+ console.error('操作失败:', error.message), process.exit(1);
865
+ }
866
+ },
867
+
868
+ // ==================== 8. 模板渲染引擎核心 ====================
869
+ /**
870
+ * 确保HTML文档类型声明位于文件开头
871
+ * @param {string} html - 渲染后的HTML内容
872
+ * @returns {string} 标准化文档
873
+ */
874
+ _ensureDoctypeFirst = html => {
875
+ return html.trim().toLowerCase().startsWith('<!doctype') ? html : `<!DOCTYPE html>\n${html}`;
876
+ },
877
+
878
+ /**
879
+ * 核心模板合成算法- 将页面模板内容合并到基础模板中
880
+ * @param {string} baseContent - 基础模板
881
+ * @param {string} templateContent - 页面模板
882
+ * @returns {string} 合成后的HTML
883
+ */
884
+ _renderTemplateContent = (baseContent, templateContent) => {
885
+ const baseBlocks = _findBlockPositions(baseContent), templateBlocks = _findBlockPositions(templateContent),
886
+ replacements = [];
887
+ let finalHtml = baseContent;
888
+
889
+ // 收集所有需要替换的区块
890
+ for (const [name, templateBlockArray] of Object.entries(templateBlocks)) {
891
+ const baseBlockArray = baseBlocks[name] || [], minLength = Math.min(templateBlockArray.length, baseBlockArray.length);
892
+
893
+ for (let i = 0; i < minLength; i++) {
894
+ const { startIndex, endIndex } = baseBlockArray[i];
895
+ replacements.push({ innerContent: templateBlockArray[i].innerContent, startIndex, endIndex });
896
+ }
897
+ }
898
+
899
+ replacements.sort((a, b) => b.startIndex - a.startIndex); // 按索引从大到小排序,避免替换时影响后续索引
900
+ // 执行替换
901
+ for (const { startIndex, innerContent, endIndex } of replacements)
902
+ finalHtml = finalHtml.slice(0, startIndex) + innerContent + finalHtml.slice(endIndex);
903
+
904
+ return _ensureDoctypeFirst(_cleanTemplateTags(finalHtml));
905
+ };
906
+
907
+ /**
908
+ * 模板文件结构验证入口
909
+ * >查看定义:@see {@link validateTemplateFile}
910
+ * @param {string} fileName - 目标文件
911
+ * @param {boolean} [isDev=false] - 开发模式标识
912
+ * @throws {Error} 校验失败时抛出异常
913
+ */
914
+ const validateTemplateFile = async (fileName, isDev = false) => {
915
+ const filePath = path.join(templatesAbsDir, fileName), content = await fsPromises.readFile(filePath, 'utf8'),
916
+ errors = _validateTemplateStructure(content);
917
+
918
+ if (errors.length > 0) {
919
+ const errorMsg = `模板 ${fileName} 结构错误:\n${errors.join('\n')}`;
920
+ if (isDev) console.error(errorMsg);
921
+ else throw new Error(errorMsg);
922
+ }
923
+ }
924
+
925
+ /**
926
+ * 模板渲染函数,处理模板继承关系
927
+ * >查看定义:@see {@link renderTemplate}
928
+ * @param {string} templateFile - 模板文件名
929
+ * @returns {Promise<string>} 渲染后的HTML内容(不包含变量替换)
930
+ * 核心流程:
931
+ * 1. 检测[extends]指令
932
+ * 2. 剥离继承指令行
933
+ * 3. 加载基模板并合并内容
934
+ * 4. 返回合成后的模板
935
+ */
936
+ const renderTemplate = async templateFile => {
937
+ const templatePath = path.join(templatesAbsDir, templateFile),
938
+ templateContent = await fsPromises.readFile(templatePath, 'utf8'), // 读取模板内容
939
+ extendsMatch = templateContent.match(_resetRegex(extendsRegex)); // 匹配[extends]指令
940
+
941
+ if (!extendsMatch) return _renderTemplateContent(templateContent, '');
942
+ const remainingContent = templateContent.slice(extendsMatch[0].length), // 移除整行(包括指令和注释)
943
+ baseTemplateFile = extendsMatch[1].trim(), basePath = path.isAbsolute(baseTemplateFile)
944
+ ? path.join(templatesAbsDir, baseTemplateFile) : path.join(path.dirname(templatePath), baseTemplateFile);
945
+
946
+ // 检查基模板是否存在
947
+ try {
948
+ await fsPromises.access(basePath);
949
+ } catch (error) {
950
+ throw new Error(`基模板文件不存在: ${baseTemplateFile} (在 ${templateFile} 中引用)`);
951
+ }
952
+ const baseContent = await fsPromises.readFile(basePath, 'utf8');
953
+ return _renderTemplateContent(baseContent, remainingContent); // 执行模板合成
954
+ }
955
+
956
+
957
+ // ==================== 9. 模块功能导出 ====================
958
+ export {
959
+ path, fsPromises, CWD, templatesDir, templatesAbsDir, staticDir, customizeDir, accountDir, defaultPort, writtenFilesToIgnore,
960
+ getAvailableTemplates, findEntryFile, validateTemplateFile, renderTemplate, processIncludes, setCompilationMode,
961
+ getIncludedFiles, processVariables, loadUserFeatures, monitorFileWrites
962
+ };