vite-plugin-fenom 1.0.1

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/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vite-plugin-fenom",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "main": "./vite-plugin-fenom.cjs",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./vite-plugin-fenom.mjs",
9
+ "require": "./vite-plugin-fenom.cjs"
10
+ }
11
+ },
12
+ "types": "./vite-plugin-fenom.d.ts",
13
+ "files": [
14
+ "vite-plugin-fenom.mjs",
15
+ "vite-plugin-fenom.cjs",
16
+ "vite-plugin-fenom.d.ts"
17
+ ],
18
+ "description": "Simple and fast template engine",
19
+ "keywords": [
20
+ "template",
21
+ "engine",
22
+ "html"
23
+ ],
24
+ "author": "@Alekseevich-psk",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "fast-glob": "^3.3.3"
28
+ }
29
+ }
package/readme.md ADDED
@@ -0,0 +1,129 @@
1
+ # ⚙️ fenom-js
2
+
3
+ JavaScript-шаблонизатор, разрабатывался как максимально близкий аналог шаблонизатора [Fenom для PHP](https://github.com/fenom-template/fenom), используемого в CMS **MODX Revolution**, чтобы верстальщики и разработчики могли работать локально без запуска сервера.
4
+
5
+ 🔧 **Синхронный**, **без зависимости от сборщиков**, работает **в браузере** и **локально** — идеален для верстки и прототипирования.
6
+
7
+ 📌 В **бета-версии**. Пригоден для тестирования и локального использования.
8
+
9
+ ---
10
+
11
+ ## 💬 Особенности
12
+
13
+ - Синтаксис, близкий к `fenom.php` (MODX Revo)
14
+ - Поддерживает:
15
+ - - Переменные: **{$var}**
16
+ - - Условия: **{if}**
17
+ - - Поддержка: **==, !=, >, <, >=, <=, ===, !==, !, &&, ||, in** (для массивов).
18
+ - - Тернарные и логические операторы: **{$age >= 18 ? 'Да' : 'Нет'}**
19
+ - - Циклы **{foreach}** + Поддержка вложенных циклов.
20
+ - - Фильтры: Применяются через **|** (как в оригинальном Fenom):
21
+ - - Исключение: **{ignore}**
22
+ - - Комментарии: **{ * комментарий * }**
23
+ - Работает **синхронно** — нет промисов, всё просто
24
+ - Работает **в браузере** и **в Node.js**
25
+ - В браузере: **без `include` и `extends`** (ограничение среды)
26
+ - Легко внедряется в HTML-страницы для локальной верстки
27
+
28
+ ---
29
+
30
+ ## 🖥️ Установка
31
+
32
+ ```bash
33
+ npm install fenom-js
34
+ ```
35
+
36
+ ## 🖥️ Или подключи напрямую в браузер:
37
+
38
+ ```html
39
+ <script type="module">
40
+ import { FenomJs } from './path/to/fenom-js/index.js';
41
+ </script>
42
+ ```
43
+
44
+ ## 🖥️Пример использования
45
+
46
+ ### В шаблонах .tpl
47
+ ```html
48
+ <body>
49
+ <h1>Привет, {$name}!</h1>
50
+ {if $isAdmin}
51
+ <p>Вы — администратор.</p>
52
+ {/if}
53
+ </body>
54
+ ```
55
+
56
+ ### Js context (В браузере)
57
+ ```javascript
58
+ import { FenomJs } from 'fenom-js';
59
+
60
+ // Получаем HTML шаблона
61
+ const templateHTML = document.body.innerHTML;
62
+
63
+ // Данные
64
+ const context = {
65
+ name: 'Анна',
66
+ isAdmin: true
67
+ };
68
+
69
+ // Рендерим
70
+ const html = FenomJs(templateHTML, context);
71
+
72
+ // Вставляем обратно
73
+ document.body.innerHTML = html;
74
+ ```
75
+
76
+ ## 🧩 vite-plugin-fenom
77
+ 📌 Это отдельный пакет, для работы с **vite** и **vituum**
78
+
79
+ ```bash
80
+ npm install vite-plugin-fenom --save-dev
81
+ ```
82
+
83
+ ## ⚙️ Конфигурация (vite.config.ts или vite.config.js)
84
+ ```ts
85
+ import { defineConfig } from 'vite';
86
+ import fenom from 'vite-plugin-fenom';
87
+
88
+ export default defineConfig({
89
+ plugins: [
90
+ fenom({
91
+ pages: 'src/pages/',
92
+ data: 'src/data/**/*.json',
93
+ root: 'src/',
94
+ })
95
+ ],
96
+ build: {
97
+ outDir: 'dist',
98
+ emptyOutDir: true,
99
+ rollupOptions: {
100
+ input: ['src/scripts/main.ts', 'src/styles/style.css'],
101
+ },
102
+ },
103
+ });
104
+ ```
105
+ ### 🗂️ Структура проекта (пример)
106
+ ```
107
+ src/demo/
108
+ ├── data/
109
+ │ ├── site.json // Глобальные переменные: {$site.title}
110
+ │ └── pages/
111
+ │ ├── index.json // Контекст для index.tpl
112
+ │ └── catalog.json // Контекст для catalog.tpl
113
+ ├── pages/
114
+ │ ├── index.tpl
115
+ │ └── catalog.tpl
116
+ ├── blocks/
117
+ │ └── header.tpl // Может использоваться через {include 'blocks/header.tpl'}
118
+ └── layouts/
119
+ └── main.tpl // Шаблон через {extends 'layouts/main.tpl'}
120
+ ```
121
+
122
+ ## Статус
123
+ 🟡 Бета-версия — API может меняться.
124
+ Приветствуются предложения, PR!
125
+
126
+ [Сообщить об ошибке](https://github.com/Alekseevich-psk/fenom-js/issues)
127
+
128
+ ## Ссылки
129
+ [🐱 GitHub](https://github.com/Alekseevich-psk/fenom-js)
@@ -0,0 +1,289 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var require$$0 = require('path');
6
+ var fs = require('fs/promises');
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
26
+
27
+ var fenomJs = {};
28
+
29
+ var hasRequiredFenomJs;
30
+
31
+ function requireFenomJs () {
32
+ if (hasRequiredFenomJs) return fenomJs;
33
+ hasRequiredFenomJs = 1;
34
+ (function (exports$1) {
35
+ Object.defineProperty(exports$1,Symbol.toStringTag,{value:"Module"});const e=require$$0,t=fs,r=[...[{type:"extends",regex:/^\{extends\s+['"]file:([^'"]+)['"]\s*\}/,process:e=>({file:e[1]})},{type:"block_open",regex:/^\{block\s+(['"])(.*?)\1\s*\}/,process:e=>({name:e[2]})},{type:"block_close",regex:/^\{\/block\}/},{type:"parent",regex:/^\{parent\}/},{type:"paste",regex:/^\{paste\s+(['"])(.*?)\1\}/,process:e=>({name:e[2]})},{type:"use",regex:/^\{use\s+(['"])(.*?)\1\}/,process:e=>({file:e[2]})}],{type:"include",regex:/^\{include\s+['"]file:([^'"]+)['"](?:\s+((?:\s*\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s}]+))+))?\s*\}/,process:e=>{const t=e[1],r=e[2],s={};if(r){const e=/(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g;let t;for(;null!==(t=e.exec(r));){const e=t[1],r=t[2]||t[3]||t[4]||"";s[e]=r;}}return {file:t,params:s}}},{type:"for",regex:/^\{(for|foreach)\s*\$(\w+(?:\.\w+)?)\s+as\s*\$(\w+)(?:\s*\|\s*reverse)?\s*\}/,process:e=>({collection:`$${e[2]}`,item:e[3],key:null,reverse:e[0].includes("| reverse")})},{type:"for",regex:/^\{(for|foreach)\s*\$(\w+(?:\.\w+)?)\s+as\s*\$(\w+)\s*=>\s*\$(\w+)(?:\s*\|\s*reverse)?\s*\}/,process:e=>({collection:`$${e[2]}`,key:e[3],item:e[4],reverse:e[0].includes("| reverse")})},{type:"endfor",regex:/^\{\/(?:for|foreach)\}/},{type:"break",regex:/^\{break\}/i},{type:"continue",regex:/^\{continue\}/i},...[{type:"switch",regex:/^\{switch\s+(.+?)\}/,process:e=>({value:e[1].trim()})},{type:"case",regex:/^\{case\s+(.+?)\}/,process:e=>({value:e[1].trim()})},{type:"default",regex:/^\{default\}/},{type:"endswitch",regex:/^\{\/switch\}/}],{type:"operator",regex:/^\{\$(\w+)\s*(\+\+|--|\+=|-=|\*=|\/=|\%=)\s*([^}]+)?\}/,process:e=>{var t;return {variable:e[1],operator:e[2],value:(null==(t=e[3])?void 0:t.trim())||"1"}}},...[{type:"if",regex:/^\{if\s+(.+?)\}/,process:e=>({condition:e[1].trim()})},{type:"elseif",regex:/^\{elseif\s+(.+?)\}/,process:e=>({condition:e[1].trim()})},{type:"else",regex:/^\{else\}/},{type:"endif",regex:/^\{\/if\}/}],{type:"ignore_block",regex:/^\{ignore\}([\s\S]*?)\{\/ignore\}/,process:e=>({content:e[1]})},...[{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*(\{[^}]*\}|\[[^}]*\])\}/,process:e=>({variable:e[1],value:e[2]})},{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*(['"])(.*?)\2\}/,process:e=>({variable:e[1],value:e[3]})},{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*([^}\s][^}]*)\}/,process:e=>({variable:e[1],value:e[2].trim()})},{type:"add",regex:/^\{add\s+\$(\w+)\s*\+\+\}/,process:e=>({variable:e[1]})},{type:"var",regex:/^\{var\s+\$(\w+)\s*=\s*(['"])(.*?)\2\}/,process:e=>({variable:e[1],value:e[3]})}],...[{type:"unset",regex:/^\{unset\s+\$(\w+)\}/,process:e=>({variable:e[1]})},{type:"comment",regex:/^\{\*\s*([\s\S]*?)\s*\*\}/}],...[{type:"cycle",regex:/^\{cycle\s+(.+?)\}/,process:e=>({values:e[1]})}],...[{type:"filter",regex:/^\{filter\s+(.+?)\}/,process:e=>({filter:e[1].trim()})},{type:"endfilter",regex:/^\{\/filter\}/},{type:"raw",regex:/^\{raw\}/},{type:"endraw",regex:/^\{\/raw\}/},{type:"autoescape",regex:/^\{autoescape\}/},{type:"endautoescape",regex:/^\{\/autoescape\}/}],...[{type:"macro",regex:/^\{macro\s+(\w+)(?:\s*\((.*?)\))?\}/,process(e){const t=e[2]?e[2].split(",").map(e=>e.trim()):[];return {name:e[1],args:t}}},{type:"endmacro",regex:/^\{\/macro\}/},{type:"import",regex:/^\{import\s+(['"])(.*?)\1\s+as\s+(\w+)\}/,process:e=>({file:e[2],alias:e[3]})}],{type:"output",regex:/^\{output\s+name\s*=\s*(['"])(.*?)\1\s*\}/,process:e=>({name:e[2],filters:[]})},{type:"output",regex:/^\{output\s+(['"])(.*?)\1\s*\}/,process:e=>({name:e[2],filters:[]})},{type:"output",regex:/^\{output\s+([^\s}]+)\s*\}/,process:e=>({name:e[1],filters:[]})},{type:"output",regex:/^\{output\s+(\$?[^}]+)\}/,process:e=>({name:e[1].trim(),filters:[]})},{type:"output",regex:/^\{\$(.+?)\}/,process:e=>{const t=e[1].trim().split("|").map(e=>e.trim());return {name:`$${t[0]}`,filters:t.slice(1)}}},{type:"output",regex:/^\{([^$].+?)\}/,process:e=>({name:e[1].trim(),filters:[]})}];function s(e){const t=[];let s=0;for(;s<e.length;){let n=false;if(e.slice(s).startsWith("{ignore}")){let r=1,o=s+8;for(;o<e.length;)if(e.length-o>=8&&e.startsWith("{ignore}",o))r++,o+=8;else if(e.length-o>=9&&e.startsWith("{/ignore}",o)){if(r--,o+=9,0===r){const r=e.slice(s+8,o-9);t.push({type:"text",value:r}),s=o,n=true;break}}else o++;n||(t.push({type:"text",value:"{ignore}"}),s+=8);continue}if("{"!==e[s]){const r=e.indexOf("{",s);if(-1===r){t.push({type:"text",value:e.slice(s)});break}r>s&&t.push({type:"text",value:e.slice(s,r)}),s=r;}for(const o of r){const r=e.slice(s).match(o.regex);if(r){const e={type:o.type,value:r[0]};if("comment"===o.type){s+=r[0].length,n=true;break}o.process&&Object.assign(e,o.process(r)),t.push(e),s+=r[0].length,n=true;break}}if(!n){const t=e.slice(s,s+30).replace(/\n/g,"↵");console.warn(`Skip unknown tag at ${s}: "${t}"`),s++;}}return t}function n(e,t){const r={type:"if",condition:e[t].condition,body:[],elseIfs:[],elseBody:[]};let s=t+1,n=0;const o=[],i=[],a=[];let l=null,u=false;for(;s<e.length;){const t=e[s];if("if"===t.type&&n++,"endif"===t.type){if(0===n)break;n--;}if(n>0)l||u?l?l.tokens.push(t):u&&a.push(t):o.push(t),s++;else if("elseif"!==t.type)if("else"!==t.type){if("endif"===t.type)break;l||u?l?l.tokens.push(t):u&&a.push(t):o.push(t),s++;}else l=null,u=true,s++;else l||u?l?l.tokens.push(t):u&&a.push(t):(l={condition:t.condition,tokens:[]},i.push(l)),s++;}return r.body=c(o),r.elseIfs=i.map(e=>({condition:e.condition,body:c(e.tokens)})),r.elseBody=c(a),{node:r,nextIndex:s+1}}function o(e,t){const r=e[t],s={type:"for",key:r.key||null,item:r.item,collection:r.collection,reverse:Boolean(r.reverse),body:[],elseBody:[]};let n=t+1,o=0,i=false;for(;n<e.length;){const t=e[n];if("for"===t.type&&o++,"endfor"===t.type){if(o>0){o--,i?s.elseBody.push(t):s.body.push(t),n++;continue}return {node:s,nextIndex:n+1}}if(o>0)i?s.elseBody.push(t):s.body.push(t),n++;else if("foreachelse"!==t.type)["break","continue"].includes(t.type),i?s.elseBody.push(t):s.body.push(t),n++;else {if(0===o){i=true,n++;continue}s.body.push(t),n++;}}throw new Error("Unclosed for loop: expected {/for}")}function i(e,t){const r={type:"switch",value:e[t].value,cases:[],defaultBody:[]};let s=t+1,n=0,o=null,i=false;for(;s<e.length;){const t=e[s];if("switch"===t.type&&n++,"endswitch"===t.type){if(n>0){n--,o&&o.body.push(t),s++;continue}return {node:r,nextIndex:s+1}}if(n>0)o&&o.body.push(t),s++;else if("case"!==t.type)if("default"!==t.type)o?o.body.push(t):i?r.defaultBody.push(t):r.cases.length>0&&r.cases[r.cases.length-1].body.push(t),s++;else {if(i)throw new Error("Duplicate {default} in switch");i=true,o=null,s++;}else o={value:t.value,body:[]},r.cases.push(o),s++;}throw new Error("Unclosed switch: expected {/switch}")}function c(e){const t=[];let r=0;for(;r<e.length;){const s=e[r];if("block_open"===s.type){const n=s.name;r++;const o=[];let i=0;for(;r<e.length;){const t=e[r];if("block_open"===t.type&&i++,"block_close"===t.type){if(0===i)break;i--;}o.push(t),r++;}const a=c(o);t.push({type:"block",name:n,body:a}),r++;continue}if("extends"!==s.type)if("include"!==s.type)if(["set","var","add"].includes(s.type))t.push({...s}),r++;else {if("if"===s.type){const{node:s,nextIndex:o}=n(e,r);t.push(s),r=o;continue}if("for"===s.type||"foreach"===s.type){const{node:s,nextIndex:n}=o(e,r);t.push(s),r=n;continue}if("switch"===s.type){const{node:s,nextIndex:n}=i(e,r);t.push(s),r=n;continue}if("output"===s.type){const e=s.value.match(/^\{\$(.+)\}$/);if(!e){t.push({type:"text",value:s.value}),r++;continue}const n=e[1].trim().split("|"),o=n[0],i=n.slice(1);t.push({type:"output",name:`$${o}`,filters:i}),r++;continue}["elseif","else","endif","endfor","endforeach","endswitch"].includes(s.type)||t.push({...s}),r++;}else t.push({...s}),r++;else t.push({...s}),r++;}return t}function a(e){if("true"===(e=e.trim()))return true;if("false"===e)return false;if("null"===e)return null;if("undefined"!==e){if(!isNaN(+e)&&!e.includes(" "))return +e;if(/^["'](.*)["']$/.test(e))return e.slice(1,-1);if(e.startsWith("[")&&e.endsWith("]"))try{return e.slice(1,-1).split(",").map(e=>e.trim()).map(a)}catch{}if(e.startsWith("{")&&e.endsWith("}"))try{return JSON.parse(e.replace(/(\w+):/g,'"$1":').replace(/'/g,'"'))}catch{}return e}}function l(e,t,r){return (...s)=>{const n=s[0];let o=false;switch(t){case "array":o=Array.isArray(n);break;case "string":o="string"==typeof n;break;case "object":o=n&&"object"==typeof n&&!Array.isArray(n);}return o||console.warn(`[Fenom] filter '${e}' should be used with ${t}, but got ${typeof n}`),r(...s)}}function u(e,t){const r=e.split(".").map(e=>e.trim());let s=t;for(const n of r){if(null==s||"object"!=typeof s)return;s=s[n];}return s}function p(e,t,r,s){let n=e;for(const i of t){const[e,...t]=i.split(":").map(e=>e.trim()),c=s[e];if("function"==typeof c){const e=t.map(e=>/^["'].*["']$/.test(e)?e.slice(1,-1):isNaN(+e)?e.startsWith("$")?u(e.slice(1),r)??"":e:+e);try{n=c(n,...e);}catch(o){n="";}}}return n}function f(e){const t=function(e){const t=[],r=/(\s+|[$a-zA-Z_]\w*(?:\.\w+)*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\d+(?:\.\d+)?|[-+*/%<>=!&|?:(){}[\]~]+|[\w-]+:)/g;let s;const n=e=>["+","-","*","/","%","==","!=","<","<=",">",">=","&&","||","!","(",")","?",":","~"].includes(e);for(;null!==(s=r.exec(e));){const e=s[0].trim();if(e)if(n(e))t.push({type:"op",value:e});else if("|"===e)t.push({type:"filter",value:e});else if(e.startsWith("$"))t.push({type:"var",value:e});else if(/^["']/.test(e)){const r=e.slice(1,-1).replace(/\\(.)/g,"$1");t.push({type:"str",value:r});}else isNaN(+e)?t.push({type:"str",value:e}):t.push({type:"num",value:e});}return t}(e);let r=0;function s(){const e=o(()=>o(()=>o(()=>o(()=>o(()=>n(),["*","/","%"]),["+","-","~"]),["<","<=",">",">="]),["==","!="]),["||","&&"]);if(r<t.length&&"?"===t[r].value){r++;const n=s();r<t.length&&":"===t[r].value&&r++;return {type:"conditional",test:e,consequent:n,alternate:s()}}return e}function n(){if(r<t.length&&["!","+","-"].includes(t[r].value)){const e=t[r].value;return r++,{type:"unary",operator:e,argument:n()}}return function(){const e=t[r];if(!e)throw new Error("Unexpected end of expression");if("num"===e.type)return r++,{type:"literal",value:+e.value};if("str"===e.type)return r++,{type:"literal",value:e.value};if("var"===e.type)return r++,{type:"variable",path:e.value.slice(1)};if("("===e.value){r++;const e=s();if(r>=t.length||")"!==t[r].value)throw new Error("Expected )");return r++,e}throw new Error(`Unexpected token: ${e.value}`)}()}function o(e,s){let n=e();for(;r<t.length&&s.includes(t[r].value);){const s=t[r].value;r++;n={type:"binary",operator:s,left:n,right:e()};}return n}return s()}function y(e,t,r){switch(e.type){case "literal":return e.value;case "variable":return u(e.path,t)??"";case "unary":const s=y(e.argument,t,r);switch(e.operator){case "!":return !s;case "+":return +s;case "-":return -s}break;case "binary":{const s=y(e.left,t,r),n=y(e.right,t,r);switch(e.operator){case "+":return s+n;case "-":return s-n;case "*":return s*n;case "/":return s/n;case "%":return s%n;case "==":return s==n;case "!=":return s!=n;case "<":return s<n;case "<=":return s<=n;case ">":return s>n;case ">=":return s>=n;case "&&":return s&&n;case "||":return s||n;case "~":return String(s)+String(n)}break}case "conditional":const n=y(e.test,t,r);return y(n?e.consequent:e.alternate,t,r);case "filter":{const s=y(e.expression,t,r),n=e.args.map(e=>y(e,t,r)),o=r[e.filter];return "function"==typeof o?o(s,...n):s}}return ""}function d(e,t,r,s){var n,o,i,c,l,g;switch(e.type){case "ignore_block":t(e.content||"");break;case "comment":case "extends":case "include":break;case "text":t(e.value);break;case "output":if(/[+\-*/%<>!=&|?:]/.test(e.name))try{let n=y(f(e.name),r,s);n=p(n,e.filters,r,s),t(String(n));}catch(h){console.warn(`Eval error: ${e.name}`,h),t("");}else {let n=u((g=e.name).startsWith("$")?g.slice(1):g,r)??"";n=p(n,e.filters,r,s),t(String(n));}break;case "set":{const{variable:t,value:n}=e;if(/[+\-*/%~]/.test(n))try{const e=f(n);r[t]=y(e,r,s);}catch(h){r[t]="";}else if(n.startsWith("$")){const e=n.slice(1);r[t]=u(e,r)??"";}else r[t]=a(n);break}case "var":void 0===r[e.variable]&&(r[e.variable]=a(e.value));break;case "add":r[e.variable]=(r[e.variable]||0)+1;break;case "if":{let c=false;try{c=!!y(f(e.condition),r,s);}catch(h){console.warn(`Condition error: ${e.condition}`,h),c=false;}if(c)for(const n of e.body)d(n,t,r,s);else if(null==(n=e.elseIfs)?void 0:n.length){let n=false;for(const o of e.elseIfs){let e=false;try{e=!!y(f(o.condition),r,s);}catch(h){console.warn(`Condition error: ${o.condition}`,h),e=false;}if(e){for(const e of o.body)d(e,t,r,s);n=true;break}}if(!n&&(null==(o=e.elseBody)?void 0:o.length))for(const o of e.elseBody)d(o,t,r,s);}else if(null==(i=e.elseBody)?void 0:i.length)for(const n of e.elseBody)d(n,t,r,s);break}case "for":{const n=u(e.collection.slice(1),r);if(Array.isArray(n)&&n.length>0){const o=e.reverse??false?[...n].reverse().keys():n.keys();for(const i of o){const o=n[i];r[e.item]=o,e.key&&(r[e.key]=i);for(const n of e.body)d(n,t,r,s);}}else if(null==(c=e.elseBody)?void 0:c.length)for(const o of e.elseBody)d(o,t,r,s);break}case "switch":{const n=r[e.value.slice(1)];let o=false;for(const i of e.cases)if(i.value===n){for(const e of i.body)d(e,t,r,s);o=true;break}if(!o&&(null==(l=e.defaultBody)?void 0:l.length))for(const i of e.defaultBody)d(i,t,r,s);break}case "operator":{const{variable:s,operator:n,value:o}=e,i=u(s,r),c="number"==typeof i?i:+i||0;let a,l;if(/^[\d.]+$/.test(o))a=parseFloat(o);else {const e=u(o,r);a="number"==typeof e?e:+e||0;}switch(n){case "++":l=c,r[s]=c+1;break;case "--":l=c,r[s]=c-1;break;case "+=":l=c,r[s]=c+a;break;case "-=":l=c,r[s]=c-a;break;case "*=":l=c,r[s]=c*a;break;case "/=":l=c,r[s]=c/a;break;case "%=":l=c,r[s]=c%a;break;default:console.warn(`Unknown operator: ${n}`),l=0;}t(String(l));break}case "block":t(`{block "${e.name}"}`);break;default:console.warn(`Unknown node type: ${e.type}`);}}function g(e,t){const r={};let n=null;for(const s of e)"extends"===s.type?n=s.file:"block"===s.type&&(r[s.name]=s.body);return n?async function(e,o){try{const i=await t(n),a=c(s(i)),l={};for(const e of a)"block"===e.type&&(l[e.name]=e.body);const u={...l,...r},p={};e.block=async r=>{if(void 0!==p[r])return p[r];const n=u[r];if(!n)return p[r]="","";let i="";const a={...e};for(const e of n)if("include"===e.type)try{const r=await t(e.file),n=c(s(r)),l={...a};if(e.params)for(const[t,s]of Object.entries(e.params))"string"==typeof s&&s.startsWith("$")?l[t]=a[s.slice(1)]:l[t]=s;for(const e of n)d(e,e=>i+=e,l,o);}catch{i+=`[Include error: ${e.file}]`;}else d(e,e=>i+=e,a,o);return p[r]=i,i};let f="";for(const r of a)if("block"===r.type)f+=await e.block(r.name);else if("include"===r.type)try{const n=await t(r.file),i=c(s(n)),a={...e};if(r.params)for(const[t,s]of Object.entries(r.params))"string"==typeof s&&s.startsWith("$")?a[t]=e[s.slice(1)]:a[t]=s;for(const e of i)d(e,e=>f+=e,a,o);}catch{f+=`[Include error: ${r.file}]`;}else d(r,e=>f+=e,e,o);return f}catch(i){return `[Render error: ${i.message}]`}}:async function(r,n){let o="";for(const i of e)if("include"===i.type)try{const e=await t(i.file),a=c(s(e)),l={...r};if(i.params)for(const[t,s]of Object.entries(i.params))"string"==typeof s&&s.startsWith("$")?l[t]=r[s.slice(1)]:l[t]=s;for(const t of a)d(t,e=>o+=e,l,n);}catch{o+=`[Include error: ${i.file}]`;}else d(i,e=>o+=e,r,n);return o}}function h(r){return async function(s){const n=e.join(r,s);if(!n.endsWith(".tpl"))throw new Error(`Template path must end with .tpl: ${s}`);try{return await t.readFile(n,"utf-8")}catch(o){if("ENOENT"===o.code)throw new Error(`Template not found: ${n}`);throw o}}}const b={upper:e=>String(e).toUpperCase(),lower:e=>String(e).toLowerCase(),capitalize:e=>{const t=String(e).trim();return 0===t.length?"":t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()},ucfirst:e=>b.capitalize(e),ucwords:e=>String(e).trim().replace(/\b\w/g,e=>e.toUpperCase()),lcfirst:e=>{const t=String(e).trim();return 0===t.length?"":t.charAt(0).toLowerCase()+t.slice(1)},trim:e=>String(e).trim(),ltrim:e=>String(e).replace(/^\s+/,""),rtrim:e=>String(e).replace(/\s+$/,""),nl2br:e=>String(e).replace(/\n/g,"<br>"),replace:(e,t,r)=>String(e).split(String(t)).join(String(r)),substr:l("substr","string",(e,t,r)=>{const s=String(e);return void 0===r?s.slice(t):s.slice(t,t+r)}),urlencode:e=>encodeURIComponent(String(e)),urldecode:e=>decodeURIComponent(String(e)),escape:l("escape","string",e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")),e:e=>b.escape(e),first:l("first","array",e=>Array.isArray(e)?e[0]:e&&"object"==typeof e?Object.values(e)[0]:""),last:l("last","array",e=>{if(Array.isArray(e))return e[e.length-1];if(e&&"object"==typeof e){const t=Object.values(e);return t[t.length-1]}return ""}),join:l("join","array",(e,t=",")=>Array.isArray(e)?e.join(t):e&&"object"==typeof e?Object.values(e).join(t):String(e)),reverse:l("reverse","array",e=>Array.isArray(e)?[...e].reverse():e),sort:l("sort","array",e=>Array.isArray(e)?[...e].sort():e),ksort:l("ksort","object",e=>{if(e&&"object"==typeof e){const t={};return Object.keys(e).sort().forEach(r=>{t[r]=e[r];}),t}return e}),unique:l("unique","array",e=>Array.isArray(e)?[...new Set(e)]:e),shuffle:l("shuffle","array",e=>{if(!Array.isArray(e))return e;const t=[...e];for(let r=t.length-1;r>0;r--){const e=Math.floor(Math.random()*(r+1));[t[r],t[e]]=[t[e],t[r]];}return t}),slice:(e,t,r)=>Array.isArray(e)||"string"==typeof e?void 0===r?e.slice(t):e.slice(t,t+r):e,merge:(e,t)=>Array.isArray(e)&&Array.isArray(t)?[...e,...t]:e,batch:(e,t)=>{if(!Array.isArray(e))return e;const r=[];for(let s=0;s<e.length;s+=t)r.push(e.slice(s,s+t));return r},keys:e=>e&&"object"==typeof e?Object.keys(e):[],values:e=>e&&"object"==typeof e?Object.values(e):[],length:e=>Array.isArray(e)?e.length:e&&"object"==typeof e?Object.keys(e).length:String(e).length,number_format:(e,t=0,r=".",s=",")=>{const n=Number(e);return isNaN(n)?"":n.toLocaleString("en-US",{minimumFractionDigits:t,maximumFractionDigits:t,useGrouping:true}).replace(/,/g,s).replace(/\./g,r)},abs:e=>Math.abs(Number(e)||0),round:(e,t=0)=>{const r=10**t;return Math.round((Number(e)||0)*r)/r},json_encode:e=>JSON.stringify(e),json_decode:e=>{try{return JSON.parse(String(e))}catch{return null}},date:(e,t="d.m.Y")=>{const r=Number(e),s=new Date(isNaN(r)?e:1e3*r);if(isNaN(s.getTime()))return console.warn(`[Fenom] filter 'date' received invalid timestamp: ${e}`),"";const n=e=>e.toString().padStart(2,"0");return t.replace(/d/g,n(s.getDate())).replace(/m/g,n(s.getMonth()+1)).replace(/Y/g,s.getFullYear().toString()).replace(/H/g,n(s.getHours())).replace(/i/g,n(s.getMinutes())).replace(/s/g,n(s.getSeconds()))},default:(e,t)=>null==e||""===e||"object"==typeof e&&0===Object.keys(e).length?t:e,raw:e=>e,var_dump:e=>`<pre>${JSON.stringify(e,null,2)}</pre>`,print_r:e=>`<pre>${e instanceof Object?JSON.stringify(e,null,2):String(e)}</pre>`};exports$1.FenomJs=async function(e,t={},r){const{root:n="./src/",loader:o,minify:i=false}=r||{},a=o||h(n);try{const r=s(e),n=g(c(r),a),o=await n(t,b);return i?function(e){return e.replace(/>\s+</g,"><").replace(/\s{2,}/g," ").replace(/(<!--.*?-->)\s+/g,"$1").trim()}(o):o}catch(l){return console.error("Template error:",l),`<span style='color:red'>[Ошибка шаблона: ${l.message}]</span>`}},exports$1.compile=g,exports$1.createAsyncLoader=h,exports$1.parse=c,exports$1.tokenize=s;
36
+ } (fenomJs));
37
+ return fenomJs;
38
+ }
39
+
40
+ var fenomJsExports = requireFenomJs();
41
+
42
+ /**
43
+ * Vite-плагин для рендеринга .tpl шаблонов через fenom-js
44
+ */
45
+ function fenomPlugin(options = {}) {
46
+ const { pages = 'src/pages', data = 'src/data/**/*.json', root = 'src', minifyHtml = true, debug = false, } = options;
47
+ let config;
48
+ let templateLoader;
49
+ if (debug)
50
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Plugin initialized', { pages, data, root });
51
+ return {
52
+ name: 'vite-plugin-fenom',
53
+ configResolved(resolvedConfig) {
54
+ config = resolvedConfig;
55
+ if (debug)
56
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Config resolved', {
57
+ mode: config.mode,
58
+ command: config.command,
59
+ root: config.root,
60
+ });
61
+ },
62
+ configureServer(server) {
63
+ if (debug)
64
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Dev server setup started...');
65
+ // Создаём загрузчик шаблонов
66
+ templateLoader = fenomJsExports.createAsyncLoader(root);
67
+ if (debug)
68
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Template loader created for root:', root);
69
+ // Наблюдаем за .tpl файлами
70
+ server.watcher.on('change', (filePath) => {
71
+ if (filePath.endsWith('.tpl')) {
72
+ if (debug)
73
+ console.log('[Fenom Plugin] 🔄 Full reload triggered:', filePath);
74
+ server.ws.send({ type: 'full-reload' });
75
+ }
76
+ });
77
+ // Обработчик запросов
78
+ const handlePageRequest = async (req, res, next) => {
79
+ const url = req.url;
80
+ if (debug)
81
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Incoming request:', url);
82
+ // Пропускаем статику, API, системные пути
83
+ if (!url ||
84
+ url.startsWith('/assets/') ||
85
+ url.startsWith('/@') ||
86
+ url.startsWith('/src/') ||
87
+ url.startsWith('/node_modules/') ||
88
+ url.startsWith('/favicon.ico') ||
89
+ (url.includes('.') && !url.endsWith('/')) ||
90
+ (url.includes('?') && url.includes('.'))) {
91
+ return next();
92
+ }
93
+ // Определяем имя страницы
94
+ let pageName = 'index';
95
+ if (url !== '/') {
96
+ pageName = url.split('?')[0].split('#')[0].replace(/^\/|\/$/g, '');
97
+ }
98
+ const templatePath = require$$0.join(pages, `${pageName}.tpl`);
99
+ const relativePath = require$$0.relative(root, templatePath);
100
+ try {
101
+ if (debug)
102
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Rendering page:', { pageName, templatePath });
103
+ const source = await templateLoader(relativePath);
104
+ const context = {
105
+ title: `${pageName.charAt(0).toUpperCase() + pageName.slice(1)} Page`,
106
+ debug,
107
+ url,
108
+ };
109
+ // Рендерим через FenomJs
110
+ let html = await fenomJsExports.FenomJs(source, context, {
111
+ loader: templateLoader,
112
+ root,
113
+ minify: minifyHtml,
114
+ });
115
+ if (config.mode === 'development') {
116
+ const hmrScript = `
117
+ <script type="module">
118
+ import "/@vite/client";
119
+ </script>`;
120
+ if (html.includes('</head>')) {
121
+ html = html.replace('</head>', hmrScript + '\n</head>');
122
+ }
123
+ else if (html.includes('<body>')) {
124
+ html = html.replace('<body>', '<body>\n' + hmrScript);
125
+ }
126
+ else {
127
+ html = hmrScript + html;
128
+ }
129
+ }
130
+ // Отправляем ответ
131
+ res.statusCode = 200;
132
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
133
+ res.end(html);
134
+ if (debug)
135
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Page sent:', url);
136
+ }
137
+ catch (err) {
138
+ if (err.message.includes('Template not found')) {
139
+ return next();
140
+ }
141
+ console.error('\x1b[36m[Fenom Plugin]\x1b[0m Rendering error:', err.message);
142
+ console.error(err);
143
+ res.statusCode = 500;
144
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
145
+ res.end(`
146
+ <h1>🔧 Ошибка рендеринга</h1>
147
+ <p><strong>${err.message}</strong></p>
148
+ <pre>${err.stack}</pre>
149
+ `);
150
+ }
151
+ };
152
+ // Вставляем middleware в начало стека
153
+ server.middlewares.stack.unshift({
154
+ route: '',
155
+ handle: handlePageRequest,
156
+ });
157
+ if (debug)
158
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Middleware inserted at top of stack');
159
+ if (debug)
160
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Watching .tpl files for HMR');
161
+ },
162
+ async buildStart() {
163
+ if (config.command !== 'build')
164
+ return;
165
+ if (debug)
166
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Build started');
167
+ },
168
+ async generateBundle(_options, bundle) {
169
+ if (config.command !== 'build')
170
+ return;
171
+ if (debug)
172
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Generating HTML files...');
173
+ const { default: fastGlob } = await Promise.resolve().then(function () { return require('./index-CDmV2arq.js'); }).then(function (n) { return n.index; });
174
+ templateLoader = fenomJsExports.createAsyncLoader(root);
175
+ const searchPath = require$$0.resolve(config.root, pages);
176
+ const pattern = require$$0.join(searchPath, '**/*.tpl').replace(/\\/g, '/');
177
+ try {
178
+ const files = await fastGlob(pattern);
179
+ if (debug)
180
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Found templates:', files);
181
+ // === Собираем входы ===
182
+ const inputEntries = config.build.rollupOptions.input;
183
+ let inputFiles = [];
184
+ if (Array.isArray(inputEntries)) {
185
+ inputFiles = inputEntries;
186
+ }
187
+ else if (typeof inputEntries === 'object' && inputEntries !== null) {
188
+ inputFiles = Object.values(inputEntries);
189
+ }
190
+ else if (typeof inputEntries === 'string') {
191
+ inputFiles = [inputEntries];
192
+ }
193
+ if (debug) {
194
+ console.log('inputFiles:', inputFiles);
195
+ }
196
+ // === Находим настоящие ассеты по расширению ===
197
+ let jsChunk = '';
198
+ const cssAssets = [];
199
+ for (const [fileName, file] of Object.entries(bundle)) {
200
+ if (file.type === 'chunk' && /\.(js|ts)$/.test(fileName)) {
201
+ jsChunk = `/${fileName}`;
202
+ }
203
+ if (file.type === 'asset' && fileName.endsWith('.css')) {
204
+ cssAssets.push(`/${fileName}`);
205
+ }
206
+ }
207
+ // === Создаём карту замен ===
208
+ const replacementMap = new Map();
209
+ for (const input of inputFiles) {
210
+ if (/\.(ts|js)$/.test(input) && jsChunk) {
211
+ replacementMap.set(input, jsChunk);
212
+ }
213
+ if (/\.css$/.test(input) && cssAssets.length > 0) {
214
+ // Берём первый CSS (или можно выбрать по имени)
215
+ replacementMap.set(input, cssAssets[0]);
216
+ }
217
+ }
218
+ if (debug) {
219
+ console.log('JS chunk found:', jsChunk);
220
+ console.log('CSS assets found:', cssAssets);
221
+ console.log('replacementMap:', Object.fromEntries(replacementMap));
222
+ }
223
+ // === Генерируем HTML ===
224
+ for (const file of files) {
225
+ const fileName = require$$0.basename(file, '.tpl');
226
+ const outputFileName = fileName === 'index' ? 'index.html' : `${fileName}.html`;
227
+ try {
228
+ const source = await fs__namespace.readFile(file, 'utf-8');
229
+ const jsonDataPath = file.replace(/\.tpl$/, '.json');
230
+ let extraContext = {};
231
+ try {
232
+ const data = await fs__namespace.readFile(jsonDataPath, 'utf-8');
233
+ extraContext = JSON.parse(data);
234
+ }
235
+ catch (_a) { }
236
+ const context = {
237
+ title: `${fileName.charAt(0).toUpperCase() + fileName.slice(1)} Page`,
238
+ debug: false,
239
+ url: '/' + (fileName === 'index' ? '' : fileName),
240
+ ...extraContext,
241
+ };
242
+ let html = await fenomJsExports.FenomJs(source, context, {
243
+ loader: templateLoader,
244
+ root,
245
+ minify: minifyHtml,
246
+ });
247
+ // === Замена путей ===
248
+ for (const [devPath, prodPath] of replacementMap) {
249
+ const fullDevPath = '/' + devPath;
250
+ const escaped = fullDevPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
251
+ // <script src="...">
252
+ const scriptRegex = new RegExp(`<script[^>]+src=["']${escaped}["'][^>]*>`, 'gi');
253
+ if (scriptRegex.test(html)) {
254
+ html = html.replace(scriptRegex, `<script type="module" src="${prodPath}"></script>`);
255
+ if (debug) {
256
+ console.log(`[Fenom Plugin] Replaced script: ${fullDevPath} → ${prodPath}`);
257
+ }
258
+ }
259
+ // <link href="...">
260
+ const linkRegex = new RegExp(`<link[^>]+href=["']${escaped}["'][^>]*>`, 'gi');
261
+ if (linkRegex.test(html)) {
262
+ html = html.replace(linkRegex, `<link rel="stylesheet" href="${prodPath}">`);
263
+ if (debug) {
264
+ console.log(`[Fenom Plugin] Replaced link: ${fullDevPath} → ${prodPath}`);
265
+ }
266
+ }
267
+ }
268
+ this.emitFile({
269
+ type: 'asset',
270
+ fileName: outputFileName,
271
+ source: html,
272
+ });
273
+ if (debug)
274
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Generated:', outputFileName);
275
+ }
276
+ catch (err) {
277
+ console.error('\x1b[31m[Fenom Plugin]\x1b[0m Error rendering:', file);
278
+ }
279
+ }
280
+ }
281
+ catch (err) {
282
+ console.error('\x1b[31m[Fenom Plugin]\x1b[0m glob error:', err);
283
+ }
284
+ },
285
+ };
286
+ }
287
+
288
+ exports.default = fenomPlugin;
289
+ //# sourceMappingURL=vite-plugin-fenom.cjs.map
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface FenomPluginOptions {
3
+ pages?: string;
4
+ data?: string;
5
+ root?: string;
6
+ minifyHtml?: boolean;
7
+ debug?: boolean;
8
+ }
9
+ /**
10
+ * Vite-плагин для рендеринга .tpl шаблонов через fenom-js
11
+ */
12
+ export default function fenomPlugin(options?: FenomPluginOptions): Plugin;
@@ -0,0 +1,267 @@
1
+ import require$$0, { resolve, join, basename, relative } from 'path';
2
+ import * as fs from 'fs/promises';
3
+ import fs__default from 'fs/promises';
4
+
5
+ var fenomJs = {};
6
+
7
+ var hasRequiredFenomJs;
8
+
9
+ function requireFenomJs () {
10
+ if (hasRequiredFenomJs) return fenomJs;
11
+ hasRequiredFenomJs = 1;
12
+ (function (exports$1) {
13
+ Object.defineProperty(exports$1,Symbol.toStringTag,{value:"Module"});const e=require$$0,t=fs__default,r=[...[{type:"extends",regex:/^\{extends\s+['"]file:([^'"]+)['"]\s*\}/,process:e=>({file:e[1]})},{type:"block_open",regex:/^\{block\s+(['"])(.*?)\1\s*\}/,process:e=>({name:e[2]})},{type:"block_close",regex:/^\{\/block\}/},{type:"parent",regex:/^\{parent\}/},{type:"paste",regex:/^\{paste\s+(['"])(.*?)\1\}/,process:e=>({name:e[2]})},{type:"use",regex:/^\{use\s+(['"])(.*?)\1\}/,process:e=>({file:e[2]})}],{type:"include",regex:/^\{include\s+['"]file:([^'"]+)['"](?:\s+((?:\s*\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s}]+))+))?\s*\}/,process:e=>{const t=e[1],r=e[2],s={};if(r){const e=/(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g;let t;for(;null!==(t=e.exec(r));){const e=t[1],r=t[2]||t[3]||t[4]||"";s[e]=r;}}return {file:t,params:s}}},{type:"for",regex:/^\{(for|foreach)\s*\$(\w+(?:\.\w+)?)\s+as\s*\$(\w+)(?:\s*\|\s*reverse)?\s*\}/,process:e=>({collection:`$${e[2]}`,item:e[3],key:null,reverse:e[0].includes("| reverse")})},{type:"for",regex:/^\{(for|foreach)\s*\$(\w+(?:\.\w+)?)\s+as\s*\$(\w+)\s*=>\s*\$(\w+)(?:\s*\|\s*reverse)?\s*\}/,process:e=>({collection:`$${e[2]}`,key:e[3],item:e[4],reverse:e[0].includes("| reverse")})},{type:"endfor",regex:/^\{\/(?:for|foreach)\}/},{type:"break",regex:/^\{break\}/i},{type:"continue",regex:/^\{continue\}/i},...[{type:"switch",regex:/^\{switch\s+(.+?)\}/,process:e=>({value:e[1].trim()})},{type:"case",regex:/^\{case\s+(.+?)\}/,process:e=>({value:e[1].trim()})},{type:"default",regex:/^\{default\}/},{type:"endswitch",regex:/^\{\/switch\}/}],{type:"operator",regex:/^\{\$(\w+)\s*(\+\+|--|\+=|-=|\*=|\/=|\%=)\s*([^}]+)?\}/,process:e=>{var t;return {variable:e[1],operator:e[2],value:(null==(t=e[3])?void 0:t.trim())||"1"}}},...[{type:"if",regex:/^\{if\s+(.+?)\}/,process:e=>({condition:e[1].trim()})},{type:"elseif",regex:/^\{elseif\s+(.+?)\}/,process:e=>({condition:e[1].trim()})},{type:"else",regex:/^\{else\}/},{type:"endif",regex:/^\{\/if\}/}],{type:"ignore_block",regex:/^\{ignore\}([\s\S]*?)\{\/ignore\}/,process:e=>({content:e[1]})},...[{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*(\{[^}]*\}|\[[^}]*\])\}/,process:e=>({variable:e[1],value:e[2]})},{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*(['"])(.*?)\2\}/,process:e=>({variable:e[1],value:e[3]})},{type:"set",regex:/^\{set\s+\$(\w+)\s*=\s*([^}\s][^}]*)\}/,process:e=>({variable:e[1],value:e[2].trim()})},{type:"add",regex:/^\{add\s+\$(\w+)\s*\+\+\}/,process:e=>({variable:e[1]})},{type:"var",regex:/^\{var\s+\$(\w+)\s*=\s*(['"])(.*?)\2\}/,process:e=>({variable:e[1],value:e[3]})}],...[{type:"unset",regex:/^\{unset\s+\$(\w+)\}/,process:e=>({variable:e[1]})},{type:"comment",regex:/^\{\*\s*([\s\S]*?)\s*\*\}/}],...[{type:"cycle",regex:/^\{cycle\s+(.+?)\}/,process:e=>({values:e[1]})}],...[{type:"filter",regex:/^\{filter\s+(.+?)\}/,process:e=>({filter:e[1].trim()})},{type:"endfilter",regex:/^\{\/filter\}/},{type:"raw",regex:/^\{raw\}/},{type:"endraw",regex:/^\{\/raw\}/},{type:"autoescape",regex:/^\{autoescape\}/},{type:"endautoescape",regex:/^\{\/autoescape\}/}],...[{type:"macro",regex:/^\{macro\s+(\w+)(?:\s*\((.*?)\))?\}/,process(e){const t=e[2]?e[2].split(",").map(e=>e.trim()):[];return {name:e[1],args:t}}},{type:"endmacro",regex:/^\{\/macro\}/},{type:"import",regex:/^\{import\s+(['"])(.*?)\1\s+as\s+(\w+)\}/,process:e=>({file:e[2],alias:e[3]})}],{type:"output",regex:/^\{output\s+name\s*=\s*(['"])(.*?)\1\s*\}/,process:e=>({name:e[2],filters:[]})},{type:"output",regex:/^\{output\s+(['"])(.*?)\1\s*\}/,process:e=>({name:e[2],filters:[]})},{type:"output",regex:/^\{output\s+([^\s}]+)\s*\}/,process:e=>({name:e[1],filters:[]})},{type:"output",regex:/^\{output\s+(\$?[^}]+)\}/,process:e=>({name:e[1].trim(),filters:[]})},{type:"output",regex:/^\{\$(.+?)\}/,process:e=>{const t=e[1].trim().split("|").map(e=>e.trim());return {name:`$${t[0]}`,filters:t.slice(1)}}},{type:"output",regex:/^\{([^$].+?)\}/,process:e=>({name:e[1].trim(),filters:[]})}];function s(e){const t=[];let s=0;for(;s<e.length;){let n=false;if(e.slice(s).startsWith("{ignore}")){let r=1,o=s+8;for(;o<e.length;)if(e.length-o>=8&&e.startsWith("{ignore}",o))r++,o+=8;else if(e.length-o>=9&&e.startsWith("{/ignore}",o)){if(r--,o+=9,0===r){const r=e.slice(s+8,o-9);t.push({type:"text",value:r}),s=o,n=true;break}}else o++;n||(t.push({type:"text",value:"{ignore}"}),s+=8);continue}if("{"!==e[s]){const r=e.indexOf("{",s);if(-1===r){t.push({type:"text",value:e.slice(s)});break}r>s&&t.push({type:"text",value:e.slice(s,r)}),s=r;}for(const o of r){const r=e.slice(s).match(o.regex);if(r){const e={type:o.type,value:r[0]};if("comment"===o.type){s+=r[0].length,n=true;break}o.process&&Object.assign(e,o.process(r)),t.push(e),s+=r[0].length,n=true;break}}if(!n){const t=e.slice(s,s+30).replace(/\n/g,"↵");console.warn(`Skip unknown tag at ${s}: "${t}"`),s++;}}return t}function n(e,t){const r={type:"if",condition:e[t].condition,body:[],elseIfs:[],elseBody:[]};let s=t+1,n=0;const o=[],i=[],a=[];let l=null,u=false;for(;s<e.length;){const t=e[s];if("if"===t.type&&n++,"endif"===t.type){if(0===n)break;n--;}if(n>0)l||u?l?l.tokens.push(t):u&&a.push(t):o.push(t),s++;else if("elseif"!==t.type)if("else"!==t.type){if("endif"===t.type)break;l||u?l?l.tokens.push(t):u&&a.push(t):o.push(t),s++;}else l=null,u=true,s++;else l||u?l?l.tokens.push(t):u&&a.push(t):(l={condition:t.condition,tokens:[]},i.push(l)),s++;}return r.body=c(o),r.elseIfs=i.map(e=>({condition:e.condition,body:c(e.tokens)})),r.elseBody=c(a),{node:r,nextIndex:s+1}}function o(e,t){const r=e[t],s={type:"for",key:r.key||null,item:r.item,collection:r.collection,reverse:Boolean(r.reverse),body:[],elseBody:[]};let n=t+1,o=0,i=false;for(;n<e.length;){const t=e[n];if("for"===t.type&&o++,"endfor"===t.type){if(o>0){o--,i?s.elseBody.push(t):s.body.push(t),n++;continue}return {node:s,nextIndex:n+1}}if(o>0)i?s.elseBody.push(t):s.body.push(t),n++;else if("foreachelse"!==t.type)["break","continue"].includes(t.type),i?s.elseBody.push(t):s.body.push(t),n++;else {if(0===o){i=true,n++;continue}s.body.push(t),n++;}}throw new Error("Unclosed for loop: expected {/for}")}function i(e,t){const r={type:"switch",value:e[t].value,cases:[],defaultBody:[]};let s=t+1,n=0,o=null,i=false;for(;s<e.length;){const t=e[s];if("switch"===t.type&&n++,"endswitch"===t.type){if(n>0){n--,o&&o.body.push(t),s++;continue}return {node:r,nextIndex:s+1}}if(n>0)o&&o.body.push(t),s++;else if("case"!==t.type)if("default"!==t.type)o?o.body.push(t):i?r.defaultBody.push(t):r.cases.length>0&&r.cases[r.cases.length-1].body.push(t),s++;else {if(i)throw new Error("Duplicate {default} in switch");i=true,o=null,s++;}else o={value:t.value,body:[]},r.cases.push(o),s++;}throw new Error("Unclosed switch: expected {/switch}")}function c(e){const t=[];let r=0;for(;r<e.length;){const s=e[r];if("block_open"===s.type){const n=s.name;r++;const o=[];let i=0;for(;r<e.length;){const t=e[r];if("block_open"===t.type&&i++,"block_close"===t.type){if(0===i)break;i--;}o.push(t),r++;}const a=c(o);t.push({type:"block",name:n,body:a}),r++;continue}if("extends"!==s.type)if("include"!==s.type)if(["set","var","add"].includes(s.type))t.push({...s}),r++;else {if("if"===s.type){const{node:s,nextIndex:o}=n(e,r);t.push(s),r=o;continue}if("for"===s.type||"foreach"===s.type){const{node:s,nextIndex:n}=o(e,r);t.push(s),r=n;continue}if("switch"===s.type){const{node:s,nextIndex:n}=i(e,r);t.push(s),r=n;continue}if("output"===s.type){const e=s.value.match(/^\{\$(.+)\}$/);if(!e){t.push({type:"text",value:s.value}),r++;continue}const n=e[1].trim().split("|"),o=n[0],i=n.slice(1);t.push({type:"output",name:`$${o}`,filters:i}),r++;continue}["elseif","else","endif","endfor","endforeach","endswitch"].includes(s.type)||t.push({...s}),r++;}else t.push({...s}),r++;else t.push({...s}),r++;}return t}function a(e){if("true"===(e=e.trim()))return true;if("false"===e)return false;if("null"===e)return null;if("undefined"!==e){if(!isNaN(+e)&&!e.includes(" "))return +e;if(/^["'](.*)["']$/.test(e))return e.slice(1,-1);if(e.startsWith("[")&&e.endsWith("]"))try{return e.slice(1,-1).split(",").map(e=>e.trim()).map(a)}catch{}if(e.startsWith("{")&&e.endsWith("}"))try{return JSON.parse(e.replace(/(\w+):/g,'"$1":').replace(/'/g,'"'))}catch{}return e}}function l(e,t,r){return (...s)=>{const n=s[0];let o=false;switch(t){case "array":o=Array.isArray(n);break;case "string":o="string"==typeof n;break;case "object":o=n&&"object"==typeof n&&!Array.isArray(n);}return o||console.warn(`[Fenom] filter '${e}' should be used with ${t}, but got ${typeof n}`),r(...s)}}function u(e,t){const r=e.split(".").map(e=>e.trim());let s=t;for(const n of r){if(null==s||"object"!=typeof s)return;s=s[n];}return s}function p(e,t,r,s){let n=e;for(const i of t){const[e,...t]=i.split(":").map(e=>e.trim()),c=s[e];if("function"==typeof c){const e=t.map(e=>/^["'].*["']$/.test(e)?e.slice(1,-1):isNaN(+e)?e.startsWith("$")?u(e.slice(1),r)??"":e:+e);try{n=c(n,...e);}catch(o){n="";}}}return n}function f(e){const t=function(e){const t=[],r=/(\s+|[$a-zA-Z_]\w*(?:\.\w+)*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\d+(?:\.\d+)?|[-+*/%<>=!&|?:(){}[\]~]+|[\w-]+:)/g;let s;const n=e=>["+","-","*","/","%","==","!=","<","<=",">",">=","&&","||","!","(",")","?",":","~"].includes(e);for(;null!==(s=r.exec(e));){const e=s[0].trim();if(e)if(n(e))t.push({type:"op",value:e});else if("|"===e)t.push({type:"filter",value:e});else if(e.startsWith("$"))t.push({type:"var",value:e});else if(/^["']/.test(e)){const r=e.slice(1,-1).replace(/\\(.)/g,"$1");t.push({type:"str",value:r});}else isNaN(+e)?t.push({type:"str",value:e}):t.push({type:"num",value:e});}return t}(e);let r=0;function s(){const e=o(()=>o(()=>o(()=>o(()=>o(()=>n(),["*","/","%"]),["+","-","~"]),["<","<=",">",">="]),["==","!="]),["||","&&"]);if(r<t.length&&"?"===t[r].value){r++;const n=s();r<t.length&&":"===t[r].value&&r++;return {type:"conditional",test:e,consequent:n,alternate:s()}}return e}function n(){if(r<t.length&&["!","+","-"].includes(t[r].value)){const e=t[r].value;return r++,{type:"unary",operator:e,argument:n()}}return function(){const e=t[r];if(!e)throw new Error("Unexpected end of expression");if("num"===e.type)return r++,{type:"literal",value:+e.value};if("str"===e.type)return r++,{type:"literal",value:e.value};if("var"===e.type)return r++,{type:"variable",path:e.value.slice(1)};if("("===e.value){r++;const e=s();if(r>=t.length||")"!==t[r].value)throw new Error("Expected )");return r++,e}throw new Error(`Unexpected token: ${e.value}`)}()}function o(e,s){let n=e();for(;r<t.length&&s.includes(t[r].value);){const s=t[r].value;r++;n={type:"binary",operator:s,left:n,right:e()};}return n}return s()}function y(e,t,r){switch(e.type){case "literal":return e.value;case "variable":return u(e.path,t)??"";case "unary":const s=y(e.argument,t,r);switch(e.operator){case "!":return !s;case "+":return +s;case "-":return -s}break;case "binary":{const s=y(e.left,t,r),n=y(e.right,t,r);switch(e.operator){case "+":return s+n;case "-":return s-n;case "*":return s*n;case "/":return s/n;case "%":return s%n;case "==":return s==n;case "!=":return s!=n;case "<":return s<n;case "<=":return s<=n;case ">":return s>n;case ">=":return s>=n;case "&&":return s&&n;case "||":return s||n;case "~":return String(s)+String(n)}break}case "conditional":const n=y(e.test,t,r);return y(n?e.consequent:e.alternate,t,r);case "filter":{const s=y(e.expression,t,r),n=e.args.map(e=>y(e,t,r)),o=r[e.filter];return "function"==typeof o?o(s,...n):s}}return ""}function d(e,t,r,s){var n,o,i,c,l,g;switch(e.type){case "ignore_block":t(e.content||"");break;case "comment":case "extends":case "include":break;case "text":t(e.value);break;case "output":if(/[+\-*/%<>!=&|?:]/.test(e.name))try{let n=y(f(e.name),r,s);n=p(n,e.filters,r,s),t(String(n));}catch(h){console.warn(`Eval error: ${e.name}`,h),t("");}else {let n=u((g=e.name).startsWith("$")?g.slice(1):g,r)??"";n=p(n,e.filters,r,s),t(String(n));}break;case "set":{const{variable:t,value:n}=e;if(/[+\-*/%~]/.test(n))try{const e=f(n);r[t]=y(e,r,s);}catch(h){r[t]="";}else if(n.startsWith("$")){const e=n.slice(1);r[t]=u(e,r)??"";}else r[t]=a(n);break}case "var":void 0===r[e.variable]&&(r[e.variable]=a(e.value));break;case "add":r[e.variable]=(r[e.variable]||0)+1;break;case "if":{let c=false;try{c=!!y(f(e.condition),r,s);}catch(h){console.warn(`Condition error: ${e.condition}`,h),c=false;}if(c)for(const n of e.body)d(n,t,r,s);else if(null==(n=e.elseIfs)?void 0:n.length){let n=false;for(const o of e.elseIfs){let e=false;try{e=!!y(f(o.condition),r,s);}catch(h){console.warn(`Condition error: ${o.condition}`,h),e=false;}if(e){for(const e of o.body)d(e,t,r,s);n=true;break}}if(!n&&(null==(o=e.elseBody)?void 0:o.length))for(const o of e.elseBody)d(o,t,r,s);}else if(null==(i=e.elseBody)?void 0:i.length)for(const n of e.elseBody)d(n,t,r,s);break}case "for":{const n=u(e.collection.slice(1),r);if(Array.isArray(n)&&n.length>0){const o=e.reverse??false?[...n].reverse().keys():n.keys();for(const i of o){const o=n[i];r[e.item]=o,e.key&&(r[e.key]=i);for(const n of e.body)d(n,t,r,s);}}else if(null==(c=e.elseBody)?void 0:c.length)for(const o of e.elseBody)d(o,t,r,s);break}case "switch":{const n=r[e.value.slice(1)];let o=false;for(const i of e.cases)if(i.value===n){for(const e of i.body)d(e,t,r,s);o=true;break}if(!o&&(null==(l=e.defaultBody)?void 0:l.length))for(const i of e.defaultBody)d(i,t,r,s);break}case "operator":{const{variable:s,operator:n,value:o}=e,i=u(s,r),c="number"==typeof i?i:+i||0;let a,l;if(/^[\d.]+$/.test(o))a=parseFloat(o);else {const e=u(o,r);a="number"==typeof e?e:+e||0;}switch(n){case "++":l=c,r[s]=c+1;break;case "--":l=c,r[s]=c-1;break;case "+=":l=c,r[s]=c+a;break;case "-=":l=c,r[s]=c-a;break;case "*=":l=c,r[s]=c*a;break;case "/=":l=c,r[s]=c/a;break;case "%=":l=c,r[s]=c%a;break;default:console.warn(`Unknown operator: ${n}`),l=0;}t(String(l));break}case "block":t(`{block "${e.name}"}`);break;default:console.warn(`Unknown node type: ${e.type}`);}}function g(e,t){const r={};let n=null;for(const s of e)"extends"===s.type?n=s.file:"block"===s.type&&(r[s.name]=s.body);return n?async function(e,o){try{const i=await t(n),a=c(s(i)),l={};for(const e of a)"block"===e.type&&(l[e.name]=e.body);const u={...l,...r},p={};e.block=async r=>{if(void 0!==p[r])return p[r];const n=u[r];if(!n)return p[r]="","";let i="";const a={...e};for(const e of n)if("include"===e.type)try{const r=await t(e.file),n=c(s(r)),l={...a};if(e.params)for(const[t,s]of Object.entries(e.params))"string"==typeof s&&s.startsWith("$")?l[t]=a[s.slice(1)]:l[t]=s;for(const e of n)d(e,e=>i+=e,l,o);}catch{i+=`[Include error: ${e.file}]`;}else d(e,e=>i+=e,a,o);return p[r]=i,i};let f="";for(const r of a)if("block"===r.type)f+=await e.block(r.name);else if("include"===r.type)try{const n=await t(r.file),i=c(s(n)),a={...e};if(r.params)for(const[t,s]of Object.entries(r.params))"string"==typeof s&&s.startsWith("$")?a[t]=e[s.slice(1)]:a[t]=s;for(const e of i)d(e,e=>f+=e,a,o);}catch{f+=`[Include error: ${r.file}]`;}else d(r,e=>f+=e,e,o);return f}catch(i){return `[Render error: ${i.message}]`}}:async function(r,n){let o="";for(const i of e)if("include"===i.type)try{const e=await t(i.file),a=c(s(e)),l={...r};if(i.params)for(const[t,s]of Object.entries(i.params))"string"==typeof s&&s.startsWith("$")?l[t]=r[s.slice(1)]:l[t]=s;for(const t of a)d(t,e=>o+=e,l,n);}catch{o+=`[Include error: ${i.file}]`;}else d(i,e=>o+=e,r,n);return o}}function h(r){return async function(s){const n=e.join(r,s);if(!n.endsWith(".tpl"))throw new Error(`Template path must end with .tpl: ${s}`);try{return await t.readFile(n,"utf-8")}catch(o){if("ENOENT"===o.code)throw new Error(`Template not found: ${n}`);throw o}}}const b={upper:e=>String(e).toUpperCase(),lower:e=>String(e).toLowerCase(),capitalize:e=>{const t=String(e).trim();return 0===t.length?"":t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()},ucfirst:e=>b.capitalize(e),ucwords:e=>String(e).trim().replace(/\b\w/g,e=>e.toUpperCase()),lcfirst:e=>{const t=String(e).trim();return 0===t.length?"":t.charAt(0).toLowerCase()+t.slice(1)},trim:e=>String(e).trim(),ltrim:e=>String(e).replace(/^\s+/,""),rtrim:e=>String(e).replace(/\s+$/,""),nl2br:e=>String(e).replace(/\n/g,"<br>"),replace:(e,t,r)=>String(e).split(String(t)).join(String(r)),substr:l("substr","string",(e,t,r)=>{const s=String(e);return void 0===r?s.slice(t):s.slice(t,t+r)}),urlencode:e=>encodeURIComponent(String(e)),urldecode:e=>decodeURIComponent(String(e)),escape:l("escape","string",e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")),e:e=>b.escape(e),first:l("first","array",e=>Array.isArray(e)?e[0]:e&&"object"==typeof e?Object.values(e)[0]:""),last:l("last","array",e=>{if(Array.isArray(e))return e[e.length-1];if(e&&"object"==typeof e){const t=Object.values(e);return t[t.length-1]}return ""}),join:l("join","array",(e,t=",")=>Array.isArray(e)?e.join(t):e&&"object"==typeof e?Object.values(e).join(t):String(e)),reverse:l("reverse","array",e=>Array.isArray(e)?[...e].reverse():e),sort:l("sort","array",e=>Array.isArray(e)?[...e].sort():e),ksort:l("ksort","object",e=>{if(e&&"object"==typeof e){const t={};return Object.keys(e).sort().forEach(r=>{t[r]=e[r];}),t}return e}),unique:l("unique","array",e=>Array.isArray(e)?[...new Set(e)]:e),shuffle:l("shuffle","array",e=>{if(!Array.isArray(e))return e;const t=[...e];for(let r=t.length-1;r>0;r--){const e=Math.floor(Math.random()*(r+1));[t[r],t[e]]=[t[e],t[r]];}return t}),slice:(e,t,r)=>Array.isArray(e)||"string"==typeof e?void 0===r?e.slice(t):e.slice(t,t+r):e,merge:(e,t)=>Array.isArray(e)&&Array.isArray(t)?[...e,...t]:e,batch:(e,t)=>{if(!Array.isArray(e))return e;const r=[];for(let s=0;s<e.length;s+=t)r.push(e.slice(s,s+t));return r},keys:e=>e&&"object"==typeof e?Object.keys(e):[],values:e=>e&&"object"==typeof e?Object.values(e):[],length:e=>Array.isArray(e)?e.length:e&&"object"==typeof e?Object.keys(e).length:String(e).length,number_format:(e,t=0,r=".",s=",")=>{const n=Number(e);return isNaN(n)?"":n.toLocaleString("en-US",{minimumFractionDigits:t,maximumFractionDigits:t,useGrouping:true}).replace(/,/g,s).replace(/\./g,r)},abs:e=>Math.abs(Number(e)||0),round:(e,t=0)=>{const r=10**t;return Math.round((Number(e)||0)*r)/r},json_encode:e=>JSON.stringify(e),json_decode:e=>{try{return JSON.parse(String(e))}catch{return null}},date:(e,t="d.m.Y")=>{const r=Number(e),s=new Date(isNaN(r)?e:1e3*r);if(isNaN(s.getTime()))return console.warn(`[Fenom] filter 'date' received invalid timestamp: ${e}`),"";const n=e=>e.toString().padStart(2,"0");return t.replace(/d/g,n(s.getDate())).replace(/m/g,n(s.getMonth()+1)).replace(/Y/g,s.getFullYear().toString()).replace(/H/g,n(s.getHours())).replace(/i/g,n(s.getMinutes())).replace(/s/g,n(s.getSeconds()))},default:(e,t)=>null==e||""===e||"object"==typeof e&&0===Object.keys(e).length?t:e,raw:e=>e,var_dump:e=>`<pre>${JSON.stringify(e,null,2)}</pre>`,print_r:e=>`<pre>${e instanceof Object?JSON.stringify(e,null,2):String(e)}</pre>`};exports$1.FenomJs=async function(e,t={},r){const{root:n="./src/",loader:o,minify:i=false}=r||{},a=o||h(n);try{const r=s(e),n=g(c(r),a),o=await n(t,b);return i?function(e){return e.replace(/>\s+</g,"><").replace(/\s{2,}/g," ").replace(/(<!--.*?-->)\s+/g,"$1").trim()}(o):o}catch(l){return console.error("Template error:",l),`<span style='color:red'>[Ошибка шаблона: ${l.message}]</span>`}},exports$1.compile=g,exports$1.createAsyncLoader=h,exports$1.parse=c,exports$1.tokenize=s;
14
+ } (fenomJs));
15
+ return fenomJs;
16
+ }
17
+
18
+ var fenomJsExports = requireFenomJs();
19
+
20
+ /**
21
+ * Vite-плагин для рендеринга .tpl шаблонов через fenom-js
22
+ */
23
+ function fenomPlugin(options = {}) {
24
+ const { pages = 'src/pages', data = 'src/data/**/*.json', root = 'src', minifyHtml = true, debug = false, } = options;
25
+ let config;
26
+ let templateLoader;
27
+ if (debug)
28
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Plugin initialized', { pages, data, root });
29
+ return {
30
+ name: 'vite-plugin-fenom',
31
+ configResolved(resolvedConfig) {
32
+ config = resolvedConfig;
33
+ if (debug)
34
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Config resolved', {
35
+ mode: config.mode,
36
+ command: config.command,
37
+ root: config.root,
38
+ });
39
+ },
40
+ configureServer(server) {
41
+ if (debug)
42
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Dev server setup started...');
43
+ // Создаём загрузчик шаблонов
44
+ templateLoader = fenomJsExports.createAsyncLoader(root);
45
+ if (debug)
46
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Template loader created for root:', root);
47
+ // Наблюдаем за .tpl файлами
48
+ server.watcher.on('change', (filePath) => {
49
+ if (filePath.endsWith('.tpl')) {
50
+ if (debug)
51
+ console.log('[Fenom Plugin] 🔄 Full reload triggered:', filePath);
52
+ server.ws.send({ type: 'full-reload' });
53
+ }
54
+ });
55
+ // Обработчик запросов
56
+ const handlePageRequest = async (req, res, next) => {
57
+ const url = req.url;
58
+ if (debug)
59
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Incoming request:', url);
60
+ // Пропускаем статику, API, системные пути
61
+ if (!url ||
62
+ url.startsWith('/assets/') ||
63
+ url.startsWith('/@') ||
64
+ url.startsWith('/src/') ||
65
+ url.startsWith('/node_modules/') ||
66
+ url.startsWith('/favicon.ico') ||
67
+ (url.includes('.') && !url.endsWith('/')) ||
68
+ (url.includes('?') && url.includes('.'))) {
69
+ return next();
70
+ }
71
+ // Определяем имя страницы
72
+ let pageName = 'index';
73
+ if (url !== '/') {
74
+ pageName = url.split('?')[0].split('#')[0].replace(/^\/|\/$/g, '');
75
+ }
76
+ const templatePath = join(pages, `${pageName}.tpl`);
77
+ const relativePath = relative(root, templatePath);
78
+ try {
79
+ if (debug)
80
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Rendering page:', { pageName, templatePath });
81
+ const source = await templateLoader(relativePath);
82
+ const context = {
83
+ title: `${pageName.charAt(0).toUpperCase() + pageName.slice(1)} Page`,
84
+ debug,
85
+ url,
86
+ };
87
+ // Рендерим через FenomJs
88
+ let html = await fenomJsExports.FenomJs(source, context, {
89
+ loader: templateLoader,
90
+ root,
91
+ minify: minifyHtml,
92
+ });
93
+ if (config.mode === 'development') {
94
+ const hmrScript = `
95
+ <script type="module">
96
+ import "/@vite/client";
97
+ </script>`;
98
+ if (html.includes('</head>')) {
99
+ html = html.replace('</head>', hmrScript + '\n</head>');
100
+ }
101
+ else if (html.includes('<body>')) {
102
+ html = html.replace('<body>', '<body>\n' + hmrScript);
103
+ }
104
+ else {
105
+ html = hmrScript + html;
106
+ }
107
+ }
108
+ // Отправляем ответ
109
+ res.statusCode = 200;
110
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
111
+ res.end(html);
112
+ if (debug)
113
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Page sent:', url);
114
+ }
115
+ catch (err) {
116
+ if (err.message.includes('Template not found')) {
117
+ return next();
118
+ }
119
+ console.error('\x1b[36m[Fenom Plugin]\x1b[0m Rendering error:', err.message);
120
+ console.error(err);
121
+ res.statusCode = 500;
122
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
123
+ res.end(`
124
+ <h1>🔧 Ошибка рендеринга</h1>
125
+ <p><strong>${err.message}</strong></p>
126
+ <pre>${err.stack}</pre>
127
+ `);
128
+ }
129
+ };
130
+ // Вставляем middleware в начало стека
131
+ server.middlewares.stack.unshift({
132
+ route: '',
133
+ handle: handlePageRequest,
134
+ });
135
+ if (debug)
136
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Middleware inserted at top of stack');
137
+ if (debug)
138
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Watching .tpl files for HMR');
139
+ },
140
+ async buildStart() {
141
+ if (config.command !== 'build')
142
+ return;
143
+ if (debug)
144
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Build started');
145
+ },
146
+ async generateBundle(_options, bundle) {
147
+ if (config.command !== 'build')
148
+ return;
149
+ if (debug)
150
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Generating HTML files...');
151
+ const { default: fastGlob } = await import('./index-CXEsIsl5.js').then(function (n) { return n.i; });
152
+ templateLoader = fenomJsExports.createAsyncLoader(root);
153
+ const searchPath = resolve(config.root, pages);
154
+ const pattern = join(searchPath, '**/*.tpl').replace(/\\/g, '/');
155
+ try {
156
+ const files = await fastGlob(pattern);
157
+ if (debug)
158
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Found templates:', files);
159
+ // === Собираем входы ===
160
+ const inputEntries = config.build.rollupOptions.input;
161
+ let inputFiles = [];
162
+ if (Array.isArray(inputEntries)) {
163
+ inputFiles = inputEntries;
164
+ }
165
+ else if (typeof inputEntries === 'object' && inputEntries !== null) {
166
+ inputFiles = Object.values(inputEntries);
167
+ }
168
+ else if (typeof inputEntries === 'string') {
169
+ inputFiles = [inputEntries];
170
+ }
171
+ if (debug) {
172
+ console.log('inputFiles:', inputFiles);
173
+ }
174
+ // === Находим настоящие ассеты по расширению ===
175
+ let jsChunk = '';
176
+ const cssAssets = [];
177
+ for (const [fileName, file] of Object.entries(bundle)) {
178
+ if (file.type === 'chunk' && /\.(js|ts)$/.test(fileName)) {
179
+ jsChunk = `/${fileName}`;
180
+ }
181
+ if (file.type === 'asset' && fileName.endsWith('.css')) {
182
+ cssAssets.push(`/${fileName}`);
183
+ }
184
+ }
185
+ // === Создаём карту замен ===
186
+ const replacementMap = new Map();
187
+ for (const input of inputFiles) {
188
+ if (/\.(ts|js)$/.test(input) && jsChunk) {
189
+ replacementMap.set(input, jsChunk);
190
+ }
191
+ if (/\.css$/.test(input) && cssAssets.length > 0) {
192
+ // Берём первый CSS (или можно выбрать по имени)
193
+ replacementMap.set(input, cssAssets[0]);
194
+ }
195
+ }
196
+ if (debug) {
197
+ console.log('JS chunk found:', jsChunk);
198
+ console.log('CSS assets found:', cssAssets);
199
+ console.log('replacementMap:', Object.fromEntries(replacementMap));
200
+ }
201
+ // === Генерируем HTML ===
202
+ for (const file of files) {
203
+ const fileName = basename(file, '.tpl');
204
+ const outputFileName = fileName === 'index' ? 'index.html' : `${fileName}.html`;
205
+ try {
206
+ const source = await fs.readFile(file, 'utf-8');
207
+ const jsonDataPath = file.replace(/\.tpl$/, '.json');
208
+ let extraContext = {};
209
+ try {
210
+ const data = await fs.readFile(jsonDataPath, 'utf-8');
211
+ extraContext = JSON.parse(data);
212
+ }
213
+ catch (_a) { }
214
+ const context = {
215
+ title: `${fileName.charAt(0).toUpperCase() + fileName.slice(1)} Page`,
216
+ debug: false,
217
+ url: '/' + (fileName === 'index' ? '' : fileName),
218
+ ...extraContext,
219
+ };
220
+ let html = await fenomJsExports.FenomJs(source, context, {
221
+ loader: templateLoader,
222
+ root,
223
+ minify: minifyHtml,
224
+ });
225
+ // === Замена путей ===
226
+ for (const [devPath, prodPath] of replacementMap) {
227
+ const fullDevPath = '/' + devPath;
228
+ const escaped = fullDevPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
229
+ // <script src="...">
230
+ const scriptRegex = new RegExp(`<script[^>]+src=["']${escaped}["'][^>]*>`, 'gi');
231
+ if (scriptRegex.test(html)) {
232
+ html = html.replace(scriptRegex, `<script type="module" src="${prodPath}"></script>`);
233
+ if (debug) {
234
+ console.log(`[Fenom Plugin] Replaced script: ${fullDevPath} → ${prodPath}`);
235
+ }
236
+ }
237
+ // <link href="...">
238
+ const linkRegex = new RegExp(`<link[^>]+href=["']${escaped}["'][^>]*>`, 'gi');
239
+ if (linkRegex.test(html)) {
240
+ html = html.replace(linkRegex, `<link rel="stylesheet" href="${prodPath}">`);
241
+ if (debug) {
242
+ console.log(`[Fenom Plugin] Replaced link: ${fullDevPath} → ${prodPath}`);
243
+ }
244
+ }
245
+ }
246
+ this.emitFile({
247
+ type: 'asset',
248
+ fileName: outputFileName,
249
+ source: html,
250
+ });
251
+ if (debug)
252
+ console.log('\x1b[36m[Fenom Plugin]\x1b[0m Generated:', outputFileName);
253
+ }
254
+ catch (err) {
255
+ console.error('\x1b[31m[Fenom Plugin]\x1b[0m Error rendering:', file);
256
+ }
257
+ }
258
+ }
259
+ catch (err) {
260
+ console.error('\x1b[31m[Fenom Plugin]\x1b[0m glob error:', err);
261
+ }
262
+ },
263
+ };
264
+ }
265
+
266
+ export { fenomPlugin as default };
267
+ //# sourceMappingURL=vite-plugin-fenom.mjs.map