vitempl 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +6 -8
- package/dist/engine-BqkJbOwH.d.ts +58 -0
- package/dist/engines/askama.d.ts +12 -0
- package/dist/engines/askama.js +28 -0
- package/dist/engines/handlebars.d.ts +5 -0
- package/dist/engines/handlebars.js +383 -0
- package/dist/engines/jinja.d.ts +11 -0
- package/dist/engines/jinja.js +27 -0
- package/dist/index.d.ts +60 -3
- package/dist/index.js +409 -0
- package/dist/jinja-like-BokTIprh.js +475 -0
- package/package.json +40 -26
- package/dist/index.cjs +0 -5
- package/dist/index.d.cts +0 -3
- package/dist/index.d.mts +0 -3
- package/dist/index.mjs +0 -3
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Stepan Sotnikov
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# vitempl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vite-плагин: собирает шаблоны для серверного рендеринга (Handlebars, askama, Jinja) из компонентов Vue (SFC). Документация — в [docs](../../docs/index.md).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- типизация шаблонов
|
|
5
|
+
## Скрипты
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
> WIP
|
|
7
|
+
- `npm run dev` — сборка с пересборкой при изменении исходников (dev-режим)
|
|
8
|
+
- `npm run build` — проверка типов и сборка в `dist/`
|
|
9
|
+
- `npm run typecheck` — проверка типов
|
|
10
|
+
- `npm run test` — запуск тестов
|
|
13
11
|
|
|
14
12
|
## Лицензия
|
|
15
13
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TemplateChildNode } from "@vue/compiler-core";
|
|
2
|
+
//#region src/engine.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Шаблонизатор, для которого собираются шаблоны.
|
|
5
|
+
*
|
|
6
|
+
* Исходники шаблонов всегда пишутся на Vue SFC, а шаблоны для сервера
|
|
7
|
+
* генерирует адаптер шаблонизатора. Для другого шаблонизатора
|
|
8
|
+
* (Jinja, Tera и т.п.) пишется свой адаптер, исходники и ядро плагина
|
|
9
|
+
* при этом не меняются.
|
|
10
|
+
*/
|
|
11
|
+
interface TemplateEngine {
|
|
12
|
+
/** Название шаблонизатора для сообщений */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Расширение собранных шаблонов, например `.hbs` */
|
|
15
|
+
extension: string;
|
|
16
|
+
/**
|
|
17
|
+
* Разбирает шаблон до генерации. Нужен шаблонизаторам, где вызов
|
|
18
|
+
* компонента зависит от самого компонента: например, в Jinja и askama
|
|
19
|
+
* компонент становится макросом, и вызывающий должен знать его параметры.
|
|
20
|
+
*/
|
|
21
|
+
analyze?(nodes: TemplateChildNode[], context: GenerateContext): TemplateInfo;
|
|
22
|
+
/** Генерирует шаблон из шаблона Vue SFC */
|
|
23
|
+
generate(nodes: TemplateChildNode[], context: GenerateContext): GenerateResult;
|
|
24
|
+
}
|
|
25
|
+
interface GenerateContext {
|
|
26
|
+
/** Импортированные компоненты: имя тега (`AppHeader`, `app-header`) → имя шаблона (`partials/AppHeader`) */
|
|
27
|
+
components: Map<string, string>;
|
|
28
|
+
/** Имя самого шаблона: путь от папки исходников без расширения */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Шаблон — вход сборки (страница или фрагмент): его данные готовит сервер */
|
|
31
|
+
entry: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Результаты `analyze` по именам шаблонов. Во время `analyze` заполняется
|
|
34
|
+
* и полным становится только к генерации, а у шаблонизаторов без `analyze`
|
|
35
|
+
* и при проверке одного файла остаётся пустым.
|
|
36
|
+
*/
|
|
37
|
+
templates: ReadonlyMap<string, TemplateInfo>;
|
|
38
|
+
}
|
|
39
|
+
interface TemplateInfo {
|
|
40
|
+
/** Данные, к которым обращается шаблон: props компонента, которые он использует */
|
|
41
|
+
params: string[];
|
|
42
|
+
/** В шаблоне есть слот */
|
|
43
|
+
hasSlot: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface GenerateResult {
|
|
46
|
+
code: string;
|
|
47
|
+
errors: TemplateDiagnostic[];
|
|
48
|
+
}
|
|
49
|
+
interface TemplateLocation {
|
|
50
|
+
line: number;
|
|
51
|
+
column: number;
|
|
52
|
+
}
|
|
53
|
+
interface TemplateDiagnostic {
|
|
54
|
+
message: string;
|
|
55
|
+
loc: TemplateLocation;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { TemplateInfo as a, TemplateEngine as i, GenerateResult as n, TemplateLocation as o, TemplateDiagnostic as r, GenerateContext as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { i as TemplateEngine } from "../engine-BqkJbOwH.js";
|
|
2
|
+
//#region src/engines/askama.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* askama: компилируемые в Rust шаблоны на основе Jinja.
|
|
5
|
+
*
|
|
6
|
+
* Шаблоны становятся частью бинарника при сборке сервера, данные приходят
|
|
7
|
+
* из структуры, поэтому выражения в них — выражения Rust: условие должно
|
|
8
|
+
* быть `bool`, а необязательное значение проверяется на null
|
|
9
|
+
* (`user != null` превращается в `{% if let Some(user) = user %}`).
|
|
10
|
+
*/
|
|
11
|
+
export declare function askama(): TemplateEngine;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as jinjaEngine } from "../jinja-like-BokTIprh.js";
|
|
2
|
+
//#region src/engines/askama.ts
|
|
3
|
+
/**
|
|
4
|
+
* askama: компилируемые в Rust шаблоны на основе Jinja.
|
|
5
|
+
*
|
|
6
|
+
* Шаблоны становятся частью бинарника при сборке сервера, данные приходят
|
|
7
|
+
* из структуры, поэтому выражения в них — выражения Rust: условие должно
|
|
8
|
+
* быть `bool`, а необязательное значение проверяется на null
|
|
9
|
+
* (`user != null` превращается в `{% if let Some(user) = user %}`).
|
|
10
|
+
*/
|
|
11
|
+
function askama() {
|
|
12
|
+
return jinjaEngine({
|
|
13
|
+
name: "askama",
|
|
14
|
+
extension: ".html",
|
|
15
|
+
elseIf: "else if",
|
|
16
|
+
assign: "let",
|
|
17
|
+
namespace: "::",
|
|
18
|
+
not: (condition) => `!${condition}`,
|
|
19
|
+
and: "&&",
|
|
20
|
+
or: "||",
|
|
21
|
+
some: (path, alias) => `let Some(${alias}) = ${path}`,
|
|
22
|
+
none: (path) => `${path}.is_none()`,
|
|
23
|
+
binds: true,
|
|
24
|
+
recursion: false
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { askama };
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { camelize, escapeHtml, isBooleanAttr, isGloballyAllowed, isVoidTag } from "@vue/shared";
|
|
2
|
+
import { ElementTypes, NodeTypes } from "@vue/compiler-core";
|
|
3
|
+
//#region src/engines/handlebars.ts
|
|
4
|
+
/** Директивы, которые обрабатываются на уровне элемента */
|
|
5
|
+
const STRUCTURAL = /* @__PURE__ */ new Set([
|
|
6
|
+
"if",
|
|
7
|
+
"else-if",
|
|
8
|
+
"else",
|
|
9
|
+
"for"
|
|
10
|
+
]);
|
|
11
|
+
/** Handlebars: компоненты становятся partials, слот по умолчанию — partial-блоком */
|
|
12
|
+
function handlebars() {
|
|
13
|
+
return {
|
|
14
|
+
name: "handlebars",
|
|
15
|
+
extension: ".hbs",
|
|
16
|
+
generate: generateHandlebars
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Генерирует шаблон Handlebars из шаблона Vue SFC.
|
|
21
|
+
*
|
|
22
|
+
* Поддерживается подмножество Vue, которое однозначно переводится
|
|
23
|
+
* в Handlebars: пути к данным, v-if/v-else-if/v-else, v-for по массивам,
|
|
24
|
+
* атрибуты, компоненты (partials) и слот по умолчанию (partial-блок).
|
|
25
|
+
* Всё остальное — ошибка компиляции.
|
|
26
|
+
*/
|
|
27
|
+
function generateHandlebars(nodes, { components }) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const error = (message, loc) => {
|
|
30
|
+
errors.push({
|
|
31
|
+
message,
|
|
32
|
+
loc: {
|
|
33
|
+
line: loc.start.line,
|
|
34
|
+
column: loc.start.column
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return "";
|
|
38
|
+
};
|
|
39
|
+
function children(nodes, scope) {
|
|
40
|
+
let out = "";
|
|
41
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
42
|
+
const node = nodes[i];
|
|
43
|
+
if (node.type !== NodeTypes.ELEMENT) {
|
|
44
|
+
out += child(node, scope);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const vIf = findDirective(node, "if");
|
|
48
|
+
if (!vIf) {
|
|
49
|
+
const vElse = findDirective(node, "else-if") ?? findDirective(node, "else");
|
|
50
|
+
out += vElse ? error("v-else без v-if", vElse.loc) : element(node, scope);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const chain = [{
|
|
54
|
+
node,
|
|
55
|
+
directive: vIf
|
|
56
|
+
}];
|
|
57
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
58
|
+
const next = nodes[j];
|
|
59
|
+
if (isBlank(next)) continue;
|
|
60
|
+
const directive = next.type === NodeTypes.ELEMENT ? findDirective(next, "else-if") ?? findDirective(next, "else") : void 0;
|
|
61
|
+
if (next.type !== NodeTypes.ELEMENT || !directive) break;
|
|
62
|
+
chain.push({
|
|
63
|
+
node: next,
|
|
64
|
+
directive
|
|
65
|
+
});
|
|
66
|
+
i = j;
|
|
67
|
+
if (directive.name === "else") break;
|
|
68
|
+
}
|
|
69
|
+
out += ifChain(chain, scope);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function child(node, scope) {
|
|
74
|
+
switch (node.type) {
|
|
75
|
+
case NodeTypes.TEXT: return escapeMustache(node.loc.source);
|
|
76
|
+
case NodeTypes.COMMENT: return "";
|
|
77
|
+
case NodeTypes.INTERPOLATION: {
|
|
78
|
+
const path = toPath(node.content, scope, node.loc);
|
|
79
|
+
return path ? `{{${path}}}` : "";
|
|
80
|
+
}
|
|
81
|
+
default: return error("неподдерживаемый узел шаблона", node.loc);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function ifChain(chain, scope) {
|
|
85
|
+
let helper = "if";
|
|
86
|
+
let out = "";
|
|
87
|
+
chain.forEach(({ node, directive }, index) => {
|
|
88
|
+
const body = element(node, scope);
|
|
89
|
+
if (directive.name === "else") {
|
|
90
|
+
out += `{{else}}${body}`;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const condition = toCondition(directive.exp, scope, directive.loc);
|
|
94
|
+
if (!condition) return;
|
|
95
|
+
if (index === 0) {
|
|
96
|
+
helper = condition.helper;
|
|
97
|
+
out += `{{#${condition.helper} ${condition.path}}}${body}`;
|
|
98
|
+
} else out += `{{else ${condition.helper} ${condition.path}}}${body}`;
|
|
99
|
+
});
|
|
100
|
+
return `${out}{{/${helper}}}`;
|
|
101
|
+
}
|
|
102
|
+
function element(node, scope) {
|
|
103
|
+
const vFor = findDirective(node, "for");
|
|
104
|
+
return vFor ? forLoop(node, vFor, scope) : elementBody(node, scope);
|
|
105
|
+
}
|
|
106
|
+
function forLoop(node, directive, scope) {
|
|
107
|
+
const result = directive.forParseResult;
|
|
108
|
+
if (!result) return error("не удалось разобрать v-for", directive.loc);
|
|
109
|
+
if (result.index) return error("v-for с тремя переменными (обход объекта) не поддерживается", directive.loc);
|
|
110
|
+
const params = [];
|
|
111
|
+
for (const param of [result.value, result.key]) {
|
|
112
|
+
if (!param) continue;
|
|
113
|
+
if (!isIdentifier(param)) return error("в v-for поддерживаются только простые имена переменных", directive.loc);
|
|
114
|
+
params.push(param.content);
|
|
115
|
+
}
|
|
116
|
+
const source = toPath(result.source, scope, directive.loc);
|
|
117
|
+
if (!source) return "";
|
|
118
|
+
const inner = {
|
|
119
|
+
...scope,
|
|
120
|
+
locals: /* @__PURE__ */ new Set([...scope.locals, ...params]),
|
|
121
|
+
depth: scope.depth + 1
|
|
122
|
+
};
|
|
123
|
+
return `{{#each ${source}${params.length ? ` as |${params.join(" ")}|` : ""}}}${elementBody(node, inner)}{{/each}}`;
|
|
124
|
+
}
|
|
125
|
+
function elementBody(node, scope) {
|
|
126
|
+
switch (node.tagType) {
|
|
127
|
+
case ElementTypes.ELEMENT: {
|
|
128
|
+
const open = `<${node.tag}${attributes(node, scope)}>`;
|
|
129
|
+
return isVoidTag(node.tag) ? open : `${open}${children(node.children, scope)}</${node.tag}>`;
|
|
130
|
+
}
|
|
131
|
+
case ElementTypes.TEMPLATE:
|
|
132
|
+
checkDirectives(node);
|
|
133
|
+
return children(node.children, scope);
|
|
134
|
+
case ElementTypes.COMPONENT: return component(node, scope);
|
|
135
|
+
case ElementTypes.SLOT: return slot(node);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function checkDirectives(node) {
|
|
139
|
+
for (const prop of node.props) if (prop.type === NodeTypes.DIRECTIVE && !STRUCTURAL.has(prop.name)) error(`директива v-${prop.name} здесь не поддерживается`, prop.loc);
|
|
140
|
+
}
|
|
141
|
+
function attributes(node, scope) {
|
|
142
|
+
const staticClass = node.props.find((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === "class");
|
|
143
|
+
const hasBoundClass = node.props.some((prop) => prop.type === NodeTypes.DIRECTIVE && bindName(prop) === "class");
|
|
144
|
+
let out = "";
|
|
145
|
+
for (const prop of node.props) {
|
|
146
|
+
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
147
|
+
if (prop !== staticClass || !hasBoundClass) out += ` ${escapeMustache(prop.loc.source)}`;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (STRUCTURAL.has(prop.name)) continue;
|
|
151
|
+
const name = bindArgument(prop);
|
|
152
|
+
if (!name || name === "key") continue;
|
|
153
|
+
if (name === "class") {
|
|
154
|
+
const value = classValue(prop, scope);
|
|
155
|
+
const base = staticClass?.type === NodeTypes.ATTRIBUTE && staticClass.value ? `${escapeHtml(staticClass.value.content)} ` : "";
|
|
156
|
+
out += ` class="${base}${value}"`;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (name === "style") {
|
|
160
|
+
error(":style не поддерживается", prop.loc);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
out += boundAttribute(name, prop, scope);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
/** Имя атрибута из v-bind, для остальных директив — ошибка */
|
|
168
|
+
function bindArgument(directive) {
|
|
169
|
+
if (directive.name !== "bind") {
|
|
170
|
+
error(`директива v-${directive.name} не поддерживается`, directive.loc);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const name = bindName(directive);
|
|
174
|
+
if (!name) {
|
|
175
|
+
error("v-bind без имени атрибута и с вычисляемым именем не поддерживается", directive.loc);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (directive.modifiers.length) {
|
|
179
|
+
error("модификаторы v-bind не поддерживаются", directive.loc);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
return name;
|
|
183
|
+
}
|
|
184
|
+
function classValue(directive, scope) {
|
|
185
|
+
const exp = directive.exp;
|
|
186
|
+
const ast = exp?.ast;
|
|
187
|
+
if (ast && ast.type === "ObjectExpression") {
|
|
188
|
+
const parts = [];
|
|
189
|
+
for (const property of ast.properties) {
|
|
190
|
+
const key = property.type === "ObjectProperty" && !property.computed ? property.key.type === "Identifier" ? property.key.name : property.key.type === "StringLiteral" ? property.key.value : void 0 : void 0;
|
|
191
|
+
const condition = key && property.type === "ObjectProperty" ? toConditionNode(property.value, scope) : void 0;
|
|
192
|
+
if (!key || !condition) {
|
|
193
|
+
error(":class поддерживает объект вида { имя: путь } или { имя: !путь }", directive.loc);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
parts.push(`{{#${condition.helper} ${condition.path}}}${escapeHtml(key)}{{/${condition.helper}}}`);
|
|
197
|
+
}
|
|
198
|
+
return parts.join(" ");
|
|
199
|
+
}
|
|
200
|
+
if (ast && ast.type === "StringLiteral") return escapeHtml(ast.value);
|
|
201
|
+
const path = toPath(exp, scope, directive.loc);
|
|
202
|
+
return path ? `{{${path}}}` : "";
|
|
203
|
+
}
|
|
204
|
+
function boundAttribute(name, directive, scope) {
|
|
205
|
+
const exp = sameNameShorthand(directive, name);
|
|
206
|
+
const ast = exp.ast;
|
|
207
|
+
if (isBooleanAttr(name)) {
|
|
208
|
+
if (ast && ast.type === "BooleanLiteral") return ast.value ? ` ${name}` : "";
|
|
209
|
+
const condition = toCondition(exp, scope, directive.loc);
|
|
210
|
+
return condition ? `{{#${condition.helper} ${condition.path}}} ${name}{{/${condition.helper}}}` : "";
|
|
211
|
+
}
|
|
212
|
+
if (ast && (ast.type === "StringLiteral" || ast.type === "NumericLiteral" || ast.type === "BooleanLiteral")) return ` ${name}="${escapeMustache(escapeHtml(String(ast.value)))}"`;
|
|
213
|
+
if (ast && ast.type === "TemplateLiteral") {
|
|
214
|
+
let value = "";
|
|
215
|
+
ast.quasis.forEach((quasi, index) => {
|
|
216
|
+
value += escapeMustache(escapeHtml(quasi.value.cooked ?? ""));
|
|
217
|
+
const expression = ast.expressions[index];
|
|
218
|
+
if (expression) {
|
|
219
|
+
const path = toPathNode(expression, scope, directive.loc);
|
|
220
|
+
value += path ? `{{${path}}}` : "";
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return ` ${name}="${value}"`;
|
|
224
|
+
}
|
|
225
|
+
const path = toPath(exp, scope, directive.loc);
|
|
226
|
+
return path ? ` ${name}="{{${path}}}"` : "";
|
|
227
|
+
}
|
|
228
|
+
function component(node, scope) {
|
|
229
|
+
const name = components.get(node.tag);
|
|
230
|
+
if (!name) return error(`компонент <${node.tag}> не импортирован в <script setup>`, node.loc);
|
|
231
|
+
const hash = [];
|
|
232
|
+
for (const prop of node.props) {
|
|
233
|
+
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
234
|
+
const key = camelize(prop.name);
|
|
235
|
+
hash.push(`${key}=${prop.value ? toStringLiteral(prop.value.content) : "true"}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (STRUCTURAL.has(prop.name)) continue;
|
|
239
|
+
const argument = bindArgument(prop);
|
|
240
|
+
if (!argument || argument === "key") continue;
|
|
241
|
+
const key = camelize(argument);
|
|
242
|
+
const value = toHashValue(sameNameShorthand(prop, key), scope, prop.loc);
|
|
243
|
+
if (value) hash.push(`${key}=${value}`);
|
|
244
|
+
}
|
|
245
|
+
const call = [
|
|
246
|
+
name,
|
|
247
|
+
"null",
|
|
248
|
+
...hash
|
|
249
|
+
].join(" ");
|
|
250
|
+
const content = node.children.filter((child) => !isBlank(child));
|
|
251
|
+
for (const child of content) if (child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.TEMPLATE && findDirective(child, "slot")) error("именованные слоты не поддерживаются", child.loc);
|
|
252
|
+
return `{{#> ${call}}}${children(content.length ? node.children : [], {
|
|
253
|
+
...scope,
|
|
254
|
+
depth: scope.depth + 1
|
|
255
|
+
})}{{/${name}}}`;
|
|
256
|
+
}
|
|
257
|
+
function slot(node) {
|
|
258
|
+
if (!node.props.every((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === "name" && prop.value?.content === "default")) return error("поддерживается только слот по умолчанию без параметров", node.loc);
|
|
259
|
+
if (node.children.some((child) => !isBlank(child))) return error("запасное содержимое слота не поддерживается", node.loc);
|
|
260
|
+
return "{{#if @partial-block}}{{> @partial-block}}{{/if}}";
|
|
261
|
+
}
|
|
262
|
+
function toCondition(exp, scope, loc) {
|
|
263
|
+
const ast = exp?.ast;
|
|
264
|
+
if (ast) {
|
|
265
|
+
const condition = toConditionNode(ast, scope);
|
|
266
|
+
if (!condition) error(`условие «${exp?.loc.source}» не поддерживается: допустимы путь к данным или его отрицание (!путь)`, loc);
|
|
267
|
+
return condition;
|
|
268
|
+
}
|
|
269
|
+
const path = toPath(exp, scope, loc);
|
|
270
|
+
return path ? {
|
|
271
|
+
helper: "if",
|
|
272
|
+
path
|
|
273
|
+
} : void 0;
|
|
274
|
+
}
|
|
275
|
+
function toConditionNode(node, scope) {
|
|
276
|
+
if (node.type === "UnaryExpression" && node.operator === "!") {
|
|
277
|
+
const path = pathOf(node.argument, scope);
|
|
278
|
+
return path ? {
|
|
279
|
+
helper: "unless",
|
|
280
|
+
path
|
|
281
|
+
} : void 0;
|
|
282
|
+
}
|
|
283
|
+
const path = pathOf(node, scope);
|
|
284
|
+
return path ? {
|
|
285
|
+
helper: "if",
|
|
286
|
+
path
|
|
287
|
+
} : void 0;
|
|
288
|
+
}
|
|
289
|
+
function toHashValue(exp, scope, loc) {
|
|
290
|
+
const ast = exp.ast;
|
|
291
|
+
if (ast && ast.type === "StringLiteral") return toStringLiteral(ast.value);
|
|
292
|
+
if (ast && (ast.type === "NumericLiteral" || ast.type === "BooleanLiteral")) return String(ast.value);
|
|
293
|
+
if (ast && ast.type === "NullLiteral") return "null";
|
|
294
|
+
return toPath(exp, scope, loc);
|
|
295
|
+
}
|
|
296
|
+
function toPath(exp, scope, loc) {
|
|
297
|
+
if (!exp || exp.type !== NodeTypes.SIMPLE_EXPRESSION) {
|
|
298
|
+
error("не удалось разобрать выражение", loc);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (exp.ast === null) return root([exp.content.trim()], scope, loc);
|
|
302
|
+
if (exp.ast === false || exp.ast === void 0) {
|
|
303
|
+
error(`не удалось разобрать выражение «${exp.content}»`, loc);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
return toPathNode(exp.ast, scope, loc, exp.content);
|
|
307
|
+
}
|
|
308
|
+
function toPathNode(node, scope, loc, source) {
|
|
309
|
+
const segments = segmentsOf(node);
|
|
310
|
+
if (!segments) {
|
|
311
|
+
error(`выражение${source ? ` «${source}»` : ""} не поддерживается: допустимы только пути к данным (a.b.c)`, loc);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
return root(segments, scope, loc);
|
|
315
|
+
}
|
|
316
|
+
/** Путь Handlebars для выражения вида a.b.c, иначе undefined без ошибки */
|
|
317
|
+
function pathOf(node, scope) {
|
|
318
|
+
const segments = segmentsOf(node);
|
|
319
|
+
return segments && root(segments, scope);
|
|
320
|
+
}
|
|
321
|
+
function root(segments, scope, loc) {
|
|
322
|
+
const [head] = segments;
|
|
323
|
+
if (head.startsWith("$") || isGloballyAllowed(head)) {
|
|
324
|
+
if (loc) error(`"${head}" не поддерживается: допустимы только данные шаблона`, loc);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (scope.locals.has(head)) return segments.join(".");
|
|
328
|
+
return "../".repeat(scope.depth) + segments.join(".");
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
code: children(nodes, {
|
|
332
|
+
locals: /* @__PURE__ */ new Set(),
|
|
333
|
+
depth: 0
|
|
334
|
+
}),
|
|
335
|
+
errors
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function segmentsOf(node) {
|
|
339
|
+
switch (node.type) {
|
|
340
|
+
case "Identifier": return [node.name];
|
|
341
|
+
case "MemberExpression":
|
|
342
|
+
case "OptionalMemberExpression": {
|
|
343
|
+
const object = segmentsOf(node.object);
|
|
344
|
+
if (!object) return;
|
|
345
|
+
if (!node.computed && node.property.type === "Identifier") return [...object, node.property.name];
|
|
346
|
+
if (node.computed && node.property.type === "NumericLiteral") return [...object, `[${node.property.value}]`];
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function findDirective(node, name) {
|
|
352
|
+
return node.props.find((prop) => prop.type === NodeTypes.DIRECTIVE && prop.name === name);
|
|
353
|
+
}
|
|
354
|
+
function bindName(directive) {
|
|
355
|
+
const arg = directive.arg;
|
|
356
|
+
return arg?.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? arg.content : void 0;
|
|
357
|
+
}
|
|
358
|
+
/** `:title` без значения — сокращение для `:title="title"` */
|
|
359
|
+
function sameNameShorthand(directive, name) {
|
|
360
|
+
return directive.exp ?? {
|
|
361
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
362
|
+
content: camelize(name),
|
|
363
|
+
isStatic: false,
|
|
364
|
+
constType: 0,
|
|
365
|
+
ast: null,
|
|
366
|
+
loc: directive.loc
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function isIdentifier(exp) {
|
|
370
|
+
return exp.type === NodeTypes.SIMPLE_EXPRESSION && /^[A-Za-z_$][\w$]*$/.test(exp.content.trim());
|
|
371
|
+
}
|
|
372
|
+
function isBlank(node) {
|
|
373
|
+
return node.type === NodeTypes.COMMENT || node.type === NodeTypes.TEXT && !node.content.trim();
|
|
374
|
+
}
|
|
375
|
+
/** Экранирует `{{` в статическом тексте, чтобы Handlebars вывел его как есть */
|
|
376
|
+
function escapeMustache(text) {
|
|
377
|
+
return text.replaceAll("{{", "\\{{");
|
|
378
|
+
}
|
|
379
|
+
function toStringLiteral(value) {
|
|
380
|
+
return `"${value.replaceAll("\"", "\\\"")}"`;
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
export { handlebars };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { i as TemplateEngine } from "../engine-BqkJbOwH.js";
|
|
2
|
+
//#region src/engines/jinja.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Jinja: шаблоны для Jinja2 (Python) и совместимых с ним движков.
|
|
5
|
+
*
|
|
6
|
+
* Шаблоны читаются во время работы сервера, данные — словарь, поэтому
|
|
7
|
+
* условие может быть любым значением, а экранирование включается
|
|
8
|
+
* на стороне сервера (`autoescape`).
|
|
9
|
+
*/
|
|
10
|
+
export declare function jinja(): TemplateEngine;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { t as jinjaEngine } from "../jinja-like-BokTIprh.js";
|
|
2
|
+
//#region src/engines/jinja.ts
|
|
3
|
+
/**
|
|
4
|
+
* Jinja: шаблоны для Jinja2 (Python) и совместимых с ним движков.
|
|
5
|
+
*
|
|
6
|
+
* Шаблоны читаются во время работы сервера, данные — словарь, поэтому
|
|
7
|
+
* условие может быть любым значением, а экранирование включается
|
|
8
|
+
* на стороне сервера (`autoescape`).
|
|
9
|
+
*/
|
|
10
|
+
function jinja() {
|
|
11
|
+
return jinjaEngine({
|
|
12
|
+
name: "jinja",
|
|
13
|
+
extension: ".html",
|
|
14
|
+
elseIf: "elif",
|
|
15
|
+
assign: "set",
|
|
16
|
+
namespace: ".",
|
|
17
|
+
not: (condition) => `not ${condition}`,
|
|
18
|
+
and: "and",
|
|
19
|
+
or: "or",
|
|
20
|
+
some: (path) => `${path} is not none`,
|
|
21
|
+
none: (path) => `${path} is none`,
|
|
22
|
+
binds: false,
|
|
23
|
+
recursion: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { jinja };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { a as TemplateInfo, i as TemplateEngine, n as GenerateResult, o as TemplateLocation, r as TemplateDiagnostic, t as GenerateContext } from "./engine-BqkJbOwH.js";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
//#region src/check.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Проверяет исходник шаблона без сборки: ошибки разбора SFC и синтаксис,
|
|
6
|
+
* который не поддерживает шаблонизатор. Те же ошибки выдаёт сборка —
|
|
7
|
+
* функция нужна инструментам вроде линтера.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkSource(source: string, engine: TemplateEngine): TemplateDiagnostic[];
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
export interface VitemplOptions {
|
|
13
|
+
/** Шаблонизатор, для которого собираются шаблоны */
|
|
14
|
+
engine: TemplateEngine;
|
|
15
|
+
/**
|
|
16
|
+
* Папка исходников шаблонов относительно root
|
|
17
|
+
* @default "src"
|
|
18
|
+
*/
|
|
19
|
+
srcDir?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Папка страниц относительно srcDir: страницы оборачиваются в shell и обрабатываются Vite как HTML
|
|
22
|
+
* @default "pages"
|
|
23
|
+
*/
|
|
24
|
+
pagesDir?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Папка фрагментов относительно srcDir: фрагменты рендерятся сервером отдельно, без shell
|
|
27
|
+
* @default "fragments"
|
|
28
|
+
*/
|
|
29
|
+
fragmentsDir?: string;
|
|
30
|
+
/**
|
|
31
|
+
* HTML-документ относительно root, в который оборачивается каждая страница.
|
|
32
|
+
* Пишется на языке шаблонизатора. Ссылки на ассеты в нём должны быть
|
|
33
|
+
* от корня (`/src/main.ts`), так как после обёртки они разрешаются
|
|
34
|
+
* от папки страницы.
|
|
35
|
+
* @default "index.html"
|
|
36
|
+
*/
|
|
37
|
+
shell?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Место в shell, куда вставляется страница
|
|
40
|
+
* @default "<!--app-html-->"
|
|
41
|
+
*/
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Папка собранных шаблонов относительно root
|
|
45
|
+
* @default "dist/templates"
|
|
46
|
+
*/
|
|
47
|
+
outDir?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Собирает шаблоны для SSR на сервере.
|
|
51
|
+
*
|
|
52
|
+
* Исходники шаблонов — Vue SFC, от страниц и фрагментов обходятся импорты
|
|
53
|
+
* компонентов, и в сборку попадают только используемые. Каждый шаблон
|
|
54
|
+
* генерируется для заданного шаблонизатора, а страницы дополнительно
|
|
55
|
+
* подаются в Vite как HTML-входы: подключения скриптов и стилей
|
|
56
|
+
* заменяются на собранные ассеты.
|
|
57
|
+
*/
|
|
58
|
+
export default function vitempl(options: VitemplOptions): Plugin;
|
|
59
|
+
//#endregion
|
|
60
|
+
export type { GenerateContext, GenerateResult, TemplateDiagnostic, TemplateEngine, TemplateInfo, TemplateLocation };
|