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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { camelize, escapeHtml, hyphenate, isBooleanAttr, isGloballyAllowed, isVoidTag } from "@vue/shared";
|
|
2
|
+
import { ElementTypes, NodeTypes } from "@vue/compiler-core";
|
|
3
|
+
//#region src/engines/jinja-like.ts
|
|
4
|
+
/** Директивы, которые обрабатываются на уровне элемента */
|
|
5
|
+
const STRUCTURAL = /* @__PURE__ */ new Set([
|
|
6
|
+
"if",
|
|
7
|
+
"else-if",
|
|
8
|
+
"else",
|
|
9
|
+
"for"
|
|
10
|
+
]);
|
|
11
|
+
/** Имя макроса, которым становится компонент */
|
|
12
|
+
const MACRO = "render";
|
|
13
|
+
/** Операторы сравнения: из выражения Vue — в выражение шаблонизатора */
|
|
14
|
+
const COMPARISON = {
|
|
15
|
+
"===": "==",
|
|
16
|
+
"==": "==",
|
|
17
|
+
"!==": "!=",
|
|
18
|
+
"!=": "!=",
|
|
19
|
+
"<": "<",
|
|
20
|
+
"<=": "<=",
|
|
21
|
+
">": ">",
|
|
22
|
+
">=": ">="
|
|
23
|
+
};
|
|
24
|
+
/** Адаптер jinja-подобного шаблонизатора из описания его диалекта */
|
|
25
|
+
function jinjaEngine(dialect) {
|
|
26
|
+
return {
|
|
27
|
+
name: dialect.name,
|
|
28
|
+
extension: dialect.extension,
|
|
29
|
+
analyze: (nodes, context) => generateJinjaLike(nodes, context, dialect).info,
|
|
30
|
+
generate: (nodes, context) => generateJinjaLike(nodes, context, dialect)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Генерирует шаблон jinja-подобного шаблонизатора из шаблона Vue SFC.
|
|
35
|
+
*
|
|
36
|
+
* Страница и фрагмент становятся обычным шаблоном: их данные готовит сервер.
|
|
37
|
+
* Остальные шаблоны становятся макросом `render`, параметры которого —
|
|
38
|
+
* данные, к которым шаблон обращается; вызывающий передаёт их по именам,
|
|
39
|
+
* а слот доходит до макроса через `{% call %}` и `caller()`.
|
|
40
|
+
*/
|
|
41
|
+
function generateJinjaLike(nodes, context, dialect) {
|
|
42
|
+
const { components, templates, entry } = context;
|
|
43
|
+
const errors = [];
|
|
44
|
+
/** Данные, к которым обращается шаблон */
|
|
45
|
+
const params = /* @__PURE__ */ new Set();
|
|
46
|
+
/** Подключённые компоненты: имя шаблона → имя, под которым он импортирован */
|
|
47
|
+
const imports = /* @__PURE__ */ new Map();
|
|
48
|
+
let hasSlot = false;
|
|
49
|
+
const error = (message, loc) => {
|
|
50
|
+
errors.push({
|
|
51
|
+
message,
|
|
52
|
+
loc: {
|
|
53
|
+
line: loc.start.line,
|
|
54
|
+
column: loc.start.column
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return "";
|
|
58
|
+
};
|
|
59
|
+
/** Имя, под которым компонент импортируется: `partials/AppHeader` → `partials_app_header` */
|
|
60
|
+
function importName(template) {
|
|
61
|
+
const existing = imports.get(template);
|
|
62
|
+
if (existing) return existing;
|
|
63
|
+
const base = template.split("/").map((segment) => hyphenate(segment).replaceAll(/[^\w]+/g, "_")).join("_");
|
|
64
|
+
let alias = base;
|
|
65
|
+
for (let i = 2; [...imports.values()].includes(alias); i++) alias = `${base}_${i}`;
|
|
66
|
+
imports.set(template, alias);
|
|
67
|
+
return alias;
|
|
68
|
+
}
|
|
69
|
+
function children(nodes, scope) {
|
|
70
|
+
let out = "";
|
|
71
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
72
|
+
const node = nodes[i];
|
|
73
|
+
if (node.type !== NodeTypes.ELEMENT) {
|
|
74
|
+
out += child(node, scope);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const vIf = findDirective(node, "if");
|
|
78
|
+
if (!vIf) {
|
|
79
|
+
const vElse = findDirective(node, "else-if") ?? findDirective(node, "else");
|
|
80
|
+
out += vElse ? error("v-else без v-if", vElse.loc) : element(node, scope);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const chain = [{
|
|
84
|
+
node,
|
|
85
|
+
directive: vIf
|
|
86
|
+
}];
|
|
87
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
88
|
+
const next = nodes[j];
|
|
89
|
+
if (isBlank(next)) continue;
|
|
90
|
+
const directive = next.type === NodeTypes.ELEMENT ? findDirective(next, "else-if") ?? findDirective(next, "else") : void 0;
|
|
91
|
+
if (next.type !== NodeTypes.ELEMENT || !directive) break;
|
|
92
|
+
chain.push({
|
|
93
|
+
node: next,
|
|
94
|
+
directive
|
|
95
|
+
});
|
|
96
|
+
i = j;
|
|
97
|
+
if (directive.name === "else") break;
|
|
98
|
+
}
|
|
99
|
+
out += ifChain(chain, scope);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function child(node, scope) {
|
|
104
|
+
switch (node.type) {
|
|
105
|
+
case NodeTypes.TEXT: return escapeDelimiters(node.loc.source);
|
|
106
|
+
case NodeTypes.COMMENT: return "";
|
|
107
|
+
case NodeTypes.INTERPOLATION: {
|
|
108
|
+
const path = toPath(node.content, scope, node.loc);
|
|
109
|
+
return path ? `{{ ${path} }}` : "";
|
|
110
|
+
}
|
|
111
|
+
default: return error("неподдерживаемый узел шаблона", node.loc);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function ifChain(chain, scope) {
|
|
115
|
+
let out = "";
|
|
116
|
+
chain.forEach(({ node, directive }, index) => {
|
|
117
|
+
if (directive.name === "else") {
|
|
118
|
+
out += `{% else %}${element(node, scope)}`;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const condition = toCondition(directive.exp, scope, directive.loc);
|
|
122
|
+
if (!condition) return;
|
|
123
|
+
const keyword = index === 0 ? "if" : dialect.elseIf;
|
|
124
|
+
out += `{% ${keyword} ${condition.code} %}${element(node, bind(scope, condition))}`;
|
|
125
|
+
});
|
|
126
|
+
return `${out}{% endif %}`;
|
|
127
|
+
}
|
|
128
|
+
function element(node, scope) {
|
|
129
|
+
const vFor = findDirective(node, "for");
|
|
130
|
+
return vFor ? forLoop(node, vFor, scope) : elementBody(node, scope);
|
|
131
|
+
}
|
|
132
|
+
function forLoop(node, directive, scope) {
|
|
133
|
+
const result = directive.forParseResult;
|
|
134
|
+
if (!result) return error("не удалось разобрать v-for", directive.loc);
|
|
135
|
+
if (result.index) return error("v-for с тремя переменными (обход объекта) не поддерживается", directive.loc);
|
|
136
|
+
if (!result.value || !isIdentifier(result.value)) return error("в v-for поддерживаются только простые имена переменных", directive.loc);
|
|
137
|
+
const item = result.value.content;
|
|
138
|
+
const locals = /* @__PURE__ */ new Set([...scope.locals, item]);
|
|
139
|
+
let index;
|
|
140
|
+
if (result.key) {
|
|
141
|
+
if (!isIdentifier(result.key)) return error("в v-for поддерживаются только простые имена переменных", directive.loc);
|
|
142
|
+
index = result.key.content;
|
|
143
|
+
locals.add(index);
|
|
144
|
+
}
|
|
145
|
+
const source = toPath(result.source, scope, directive.loc);
|
|
146
|
+
if (!source) return "";
|
|
147
|
+
const inner = {
|
|
148
|
+
locals,
|
|
149
|
+
aliases: scope.aliases
|
|
150
|
+
};
|
|
151
|
+
return `{% for ${item} in ${source} %}${index ? `{% ${dialect.assign} ${index} = loop.index0 %}` : ""}${elementBody(node, inner)}{% endfor %}`;
|
|
152
|
+
}
|
|
153
|
+
function elementBody(node, scope) {
|
|
154
|
+
switch (node.tagType) {
|
|
155
|
+
case ElementTypes.ELEMENT: {
|
|
156
|
+
const open = `<${node.tag}${attributes(node, scope)}>`;
|
|
157
|
+
return isVoidTag(node.tag) ? open : `${open}${children(node.children, scope)}</${node.tag}>`;
|
|
158
|
+
}
|
|
159
|
+
case ElementTypes.TEMPLATE:
|
|
160
|
+
checkDirectives(node);
|
|
161
|
+
return children(node.children, scope);
|
|
162
|
+
case ElementTypes.COMPONENT: return component(node, scope);
|
|
163
|
+
case ElementTypes.SLOT: return slot(node);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function checkDirectives(node) {
|
|
167
|
+
for (const prop of node.props) if (prop.type === NodeTypes.DIRECTIVE && !STRUCTURAL.has(prop.name)) error(`директива v-${prop.name} здесь не поддерживается`, prop.loc);
|
|
168
|
+
}
|
|
169
|
+
function attributes(node, scope) {
|
|
170
|
+
const staticClass = node.props.find((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === "class");
|
|
171
|
+
const hasBoundClass = node.props.some((prop) => prop.type === NodeTypes.DIRECTIVE && bindName(prop) === "class");
|
|
172
|
+
let out = "";
|
|
173
|
+
for (const prop of node.props) {
|
|
174
|
+
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
175
|
+
if (prop !== staticClass || !hasBoundClass) out += ` ${escapeDelimiters(prop.loc.source)}`;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (STRUCTURAL.has(prop.name)) continue;
|
|
179
|
+
const name = bindArgument(prop);
|
|
180
|
+
if (!name || name === "key") continue;
|
|
181
|
+
if (name === "class") {
|
|
182
|
+
const value = classValue(prop, scope);
|
|
183
|
+
const base = staticClass?.type === NodeTypes.ATTRIBUTE && staticClass.value ? `${escapeHtml(staticClass.value.content)} ` : "";
|
|
184
|
+
out += ` class="${base}${value}"`;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (name === "style") {
|
|
188
|
+
error(":style не поддерживается", prop.loc);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
out += boundAttribute(name, prop, scope);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
/** Имя атрибута из v-bind, для остальных директив — ошибка */
|
|
196
|
+
function bindArgument(directive) {
|
|
197
|
+
if (directive.name !== "bind") {
|
|
198
|
+
error(`директива v-${directive.name} не поддерживается`, directive.loc);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const name = bindName(directive);
|
|
202
|
+
if (!name) {
|
|
203
|
+
error("v-bind без имени атрибута и с вычисляемым именем не поддерживается", directive.loc);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (directive.modifiers.length) {
|
|
207
|
+
error("модификаторы v-bind не поддерживаются", directive.loc);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
return name;
|
|
211
|
+
}
|
|
212
|
+
function classValue(directive, scope) {
|
|
213
|
+
const exp = directive.exp;
|
|
214
|
+
const ast = exp?.ast;
|
|
215
|
+
if (ast && ast.type === "ObjectExpression") {
|
|
216
|
+
const parts = [];
|
|
217
|
+
for (const property of ast.properties) {
|
|
218
|
+
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;
|
|
219
|
+
const condition = key && property.type === "ObjectProperty" ? toConditionNode(property.value, scope) : void 0;
|
|
220
|
+
if (!key || !condition) {
|
|
221
|
+
error(":class поддерживает объект вида { имя: условие }", directive.loc);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
parts.push(`{% if ${condition.code} %}${escapeHtml(key)}{% endif %}`);
|
|
225
|
+
}
|
|
226
|
+
return parts.join(" ");
|
|
227
|
+
}
|
|
228
|
+
if (ast && ast.type === "StringLiteral") return escapeHtml(ast.value);
|
|
229
|
+
const path = toPath(exp, scope, directive.loc);
|
|
230
|
+
return path ? `{{ ${path} }}` : "";
|
|
231
|
+
}
|
|
232
|
+
function boundAttribute(name, directive, scope) {
|
|
233
|
+
const exp = sameNameShorthand(directive, name);
|
|
234
|
+
const ast = exp.ast;
|
|
235
|
+
if (isBooleanAttr(name)) {
|
|
236
|
+
if (ast && ast.type === "BooleanLiteral") return ast.value ? ` ${name}` : "";
|
|
237
|
+
const condition = toCondition(exp, scope, directive.loc);
|
|
238
|
+
return condition ? `{% if ${condition.code} %} ${name}{% endif %}` : "";
|
|
239
|
+
}
|
|
240
|
+
if (ast && (ast.type === "StringLiteral" || ast.type === "NumericLiteral" || ast.type === "BooleanLiteral")) return ` ${name}="${escapeDelimiters(escapeHtml(String(ast.value)))}"`;
|
|
241
|
+
if (ast && ast.type === "TemplateLiteral") {
|
|
242
|
+
let value = "";
|
|
243
|
+
ast.quasis.forEach((quasi, index) => {
|
|
244
|
+
value += escapeDelimiters(escapeHtml(quasi.value.cooked ?? ""));
|
|
245
|
+
const expression = ast.expressions[index];
|
|
246
|
+
if (expression) {
|
|
247
|
+
const path = toPathNode(expression, scope, directive.loc);
|
|
248
|
+
value += path ? `{{ ${path} }}` : "";
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return ` ${name}="${value}"`;
|
|
252
|
+
}
|
|
253
|
+
const path = toPath(exp, scope, directive.loc);
|
|
254
|
+
return path ? ` ${name}="{{ ${path} }}"` : "";
|
|
255
|
+
}
|
|
256
|
+
function component(node, scope) {
|
|
257
|
+
const template = components.get(node.tag);
|
|
258
|
+
if (!template) return error(`компонент <${node.tag}> не импортирован в <script setup>`, node.loc);
|
|
259
|
+
const self = template === context.name;
|
|
260
|
+
if (self && !dialect.recursion) return error(`${dialect.name} не поддерживает компонент, который подключает сам себя`, node.loc);
|
|
261
|
+
const props = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const prop of node.props) {
|
|
263
|
+
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
264
|
+
props.set(camelize(prop.name), prop.value ? toStringLiteral(prop.value.content) : "true");
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (STRUCTURAL.has(prop.name)) continue;
|
|
268
|
+
const argument = bindArgument(prop);
|
|
269
|
+
if (!argument || argument === "key") continue;
|
|
270
|
+
const key = camelize(argument);
|
|
271
|
+
const value = toValue(sameNameShorthand(prop, key), scope, prop.loc);
|
|
272
|
+
if (value) props.set(key, value);
|
|
273
|
+
}
|
|
274
|
+
const content = node.children.filter((child) => !isBlank(child));
|
|
275
|
+
for (const child of content) if (child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.TEMPLATE && findDirective(child, "slot")) error("именованные слоты не поддерживаются", child.loc);
|
|
276
|
+
const body = children(content, scope);
|
|
277
|
+
const info = templates.get(template);
|
|
278
|
+
const names = info ? info.params : [...props.keys()];
|
|
279
|
+
const args = [];
|
|
280
|
+
for (const param of names) {
|
|
281
|
+
const value = props.get(param);
|
|
282
|
+
if (value === void 0) {
|
|
283
|
+
error(`компонент <${node.tag}> обращается к данным "${param}", но они не переданы`, node.loc);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
args.push(`${param}=${value}`);
|
|
287
|
+
}
|
|
288
|
+
const call = self ? `${MACRO}(${args.join(", ")})` : `${importName(template)}${dialect.namespace}${MACRO}(${args.join(", ")})`;
|
|
289
|
+
if (info ? info.hasSlot : body !== "") return `{% call ${call} %}${body}{% endcall %}`;
|
|
290
|
+
if (body !== "") error(`в шаблоне компонента <${node.tag}> нет слота`, node.loc);
|
|
291
|
+
return `{{ ${call} }}`;
|
|
292
|
+
}
|
|
293
|
+
function slot(node) {
|
|
294
|
+
if (!node.props.every((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === "name" && prop.value?.content === "default")) return error("поддерживается только слот по умолчанию без параметров", node.loc);
|
|
295
|
+
if (node.children.some((child) => !isBlank(child))) return error("запасное содержимое слота не поддерживается", node.loc);
|
|
296
|
+
if (entry) return error("слот поддерживается только в компоненте: страницу и фрагмент сервер рендерит целиком", node.loc);
|
|
297
|
+
hasSlot = true;
|
|
298
|
+
return "{{ caller() }}";
|
|
299
|
+
}
|
|
300
|
+
/** Область видимости внутри ветки условия: связанное значение видно как переменная */
|
|
301
|
+
function bind(scope, condition) {
|
|
302
|
+
if (!condition.binding) return scope;
|
|
303
|
+
const { path, alias } = condition.binding;
|
|
304
|
+
return {
|
|
305
|
+
locals: /* @__PURE__ */ new Set([...scope.locals, alias]),
|
|
306
|
+
aliases: new Map([...scope.aliases, [path, alias]])
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function toCondition(exp, scope, loc) {
|
|
310
|
+
const ast = exp?.ast;
|
|
311
|
+
if (ast) {
|
|
312
|
+
const condition = toConditionNode(ast, scope);
|
|
313
|
+
if (!condition) error(`условие «${exp?.loc.source}» не поддерживается: допустимы путь к данным, его отрицание, сравнение с литералом и проверка на null`, loc);
|
|
314
|
+
return condition;
|
|
315
|
+
}
|
|
316
|
+
const path = toPath(exp, scope, loc);
|
|
317
|
+
return path ? { code: path } : void 0;
|
|
318
|
+
}
|
|
319
|
+
function toConditionNode(node, scope) {
|
|
320
|
+
if (node.type === "UnaryExpression" && node.operator === "!") {
|
|
321
|
+
const path = pathOf(node.argument, scope);
|
|
322
|
+
return path ? { code: dialect.not(path) } : void 0;
|
|
323
|
+
}
|
|
324
|
+
if (node.type === "LogicalExpression") {
|
|
325
|
+
const left = toConditionNode(node.left, scope);
|
|
326
|
+
const right = toConditionNode(node.right, scope);
|
|
327
|
+
if (!left || !right || left.binding || right.binding) return;
|
|
328
|
+
const operator = node.operator === "&&" ? dialect.and : node.operator === "||" ? dialect.or : void 0;
|
|
329
|
+
return operator ? { code: `(${left.code}) ${operator} (${right.code})` } : void 0;
|
|
330
|
+
}
|
|
331
|
+
if (node.type === "BinaryExpression") {
|
|
332
|
+
const path = pathOf(node.left, scope);
|
|
333
|
+
if (path && isNullish(node.right)) {
|
|
334
|
+
const alias = aliasFor(path, scope);
|
|
335
|
+
if (node.operator === "!==" || node.operator === "!=") return dialect.binds ? {
|
|
336
|
+
code: dialect.some(path, alias),
|
|
337
|
+
binding: {
|
|
338
|
+
path,
|
|
339
|
+
alias
|
|
340
|
+
}
|
|
341
|
+
} : { code: dialect.some(path, alias) };
|
|
342
|
+
if (node.operator === "===" || node.operator === "==") return { code: dialect.none(path) };
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const operator = COMPARISON[node.operator];
|
|
346
|
+
const left = path ?? literalOf(node.left);
|
|
347
|
+
const right = pathOf(node.right, scope) ?? literalOf(node.right);
|
|
348
|
+
return operator && left && right ? { code: `${left} ${operator} ${right}` } : void 0;
|
|
349
|
+
}
|
|
350
|
+
const path = pathOf(node, scope);
|
|
351
|
+
return path ? { code: path } : void 0;
|
|
352
|
+
}
|
|
353
|
+
/** Имя переменной для связанного значения: последний сегмент пути */
|
|
354
|
+
function aliasFor(path, scope) {
|
|
355
|
+
const base = path.split(".").pop().replaceAll(/[^\w]/g, "") || "value";
|
|
356
|
+
let alias = base;
|
|
357
|
+
for (let i = 2; scope.locals.has(alias); i++) alias = `${base}_${i}`;
|
|
358
|
+
return alias;
|
|
359
|
+
}
|
|
360
|
+
/** Значение props компонента: литерал или путь к данным */
|
|
361
|
+
function toValue(exp, scope, loc) {
|
|
362
|
+
return (exp.ast ? literalOf(exp.ast) : void 0) ?? toPath(exp, scope, loc);
|
|
363
|
+
}
|
|
364
|
+
function toPath(exp, scope, loc) {
|
|
365
|
+
if (!exp || exp.type !== NodeTypes.SIMPLE_EXPRESSION) {
|
|
366
|
+
error("не удалось разобрать выражение", loc);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (exp.ast === null) return resolve([exp.content.trim()], scope, loc);
|
|
370
|
+
if (exp.ast === false || exp.ast === void 0) {
|
|
371
|
+
error(`не удалось разобрать выражение «${exp.content}»`, loc);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
return toPathNode(exp.ast, scope, loc, exp.content);
|
|
375
|
+
}
|
|
376
|
+
function toPathNode(node, scope, loc, source) {
|
|
377
|
+
const segments = segmentsOf(node);
|
|
378
|
+
if (!segments) {
|
|
379
|
+
error(`выражение${source ? ` «${source}»` : ""} не поддерживается: допустимы только пути к данным (a.b.c)`, loc);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
return resolve(segments, scope, loc);
|
|
383
|
+
}
|
|
384
|
+
/** Путь к данным для выражения вида a.b.c, иначе undefined без ошибки */
|
|
385
|
+
function pathOf(node, scope) {
|
|
386
|
+
const segments = segmentsOf(node);
|
|
387
|
+
return segments && resolve(segments, scope);
|
|
388
|
+
}
|
|
389
|
+
function resolve(segments, scope, loc) {
|
|
390
|
+
const [head] = segments;
|
|
391
|
+
if (head.startsWith("$") || isGloballyAllowed(head)) {
|
|
392
|
+
if (loc) error(`"${head}" не поддерживается: допустимы только данные шаблона`, loc);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
for (let i = segments.length; i > 0; i--) {
|
|
396
|
+
const alias = scope.aliases.get(joinPath(segments.slice(0, i)));
|
|
397
|
+
if (alias) return joinPath([alias, ...segments.slice(i)]);
|
|
398
|
+
}
|
|
399
|
+
if (!scope.locals.has(head)) params.add(head);
|
|
400
|
+
return joinPath(segments);
|
|
401
|
+
}
|
|
402
|
+
const code = children(nodes, {
|
|
403
|
+
locals: /* @__PURE__ */ new Set(),
|
|
404
|
+
aliases: /* @__PURE__ */ new Map()
|
|
405
|
+
});
|
|
406
|
+
const info = {
|
|
407
|
+
params: [...params].sort(),
|
|
408
|
+
hasSlot
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
code: [...imports].map(([template, alias]) => `{% import "${template}${dialect.extension}" as ${alias} %}`).join("") + (entry ? code : `{% macro ${MACRO}(${info.params.join(", ")}) %}${code}{% endmacro %}`),
|
|
412
|
+
errors,
|
|
413
|
+
info
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function joinPath(segments) {
|
|
417
|
+
return segments.reduce((out, segment) => segment.startsWith("[") ? out + segment : out ? `${out}.${segment}` : segment, "");
|
|
418
|
+
}
|
|
419
|
+
function segmentsOf(node) {
|
|
420
|
+
switch (node.type) {
|
|
421
|
+
case "Identifier": return [node.name];
|
|
422
|
+
case "MemberExpression":
|
|
423
|
+
case "OptionalMemberExpression": {
|
|
424
|
+
const object = segmentsOf(node.object);
|
|
425
|
+
if (!object) return;
|
|
426
|
+
if (!node.computed && node.property.type === "Identifier") return [...object, node.property.name];
|
|
427
|
+
if (node.computed && node.property.type === "NumericLiteral") return [...object, `[${node.property.value}]`];
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Литерал шаблонизатора для литерала Vue */
|
|
433
|
+
function literalOf(node) {
|
|
434
|
+
switch (node.type) {
|
|
435
|
+
case "StringLiteral": return toStringLiteral(node.value);
|
|
436
|
+
case "NumericLiteral": return String(node.value);
|
|
437
|
+
case "BooleanLiteral": return String(node.value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function isNullish(node) {
|
|
441
|
+
return node.type === "NullLiteral" || node.type === "Identifier" && node.name === "undefined";
|
|
442
|
+
}
|
|
443
|
+
function findDirective(node, name) {
|
|
444
|
+
return node.props.find((prop) => prop.type === NodeTypes.DIRECTIVE && prop.name === name);
|
|
445
|
+
}
|
|
446
|
+
function bindName(directive) {
|
|
447
|
+
const arg = directive.arg;
|
|
448
|
+
return arg?.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? arg.content : void 0;
|
|
449
|
+
}
|
|
450
|
+
/** `:title` без значения — сокращение для `:title="title"` */
|
|
451
|
+
function sameNameShorthand(directive, name) {
|
|
452
|
+
return directive.exp ?? {
|
|
453
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
454
|
+
content: camelize(name),
|
|
455
|
+
isStatic: false,
|
|
456
|
+
constType: 0,
|
|
457
|
+
ast: null,
|
|
458
|
+
loc: directive.loc
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function isIdentifier(exp) {
|
|
462
|
+
return exp.type === NodeTypes.SIMPLE_EXPRESSION && /^[A-Za-z_$][\w$]*$/.test(exp.content.trim());
|
|
463
|
+
}
|
|
464
|
+
function isBlank(node) {
|
|
465
|
+
return node.type === NodeTypes.COMMENT || node.type === NodeTypes.TEXT && !node.content.trim();
|
|
466
|
+
}
|
|
467
|
+
/** Экранирует разделители шаблонизатора в статическом тексте */
|
|
468
|
+
function escapeDelimiters(text) {
|
|
469
|
+
return text.replaceAll(/\{[{%#]/g, (match) => `{{ "${match}" }}`);
|
|
470
|
+
}
|
|
471
|
+
function toStringLiteral(value) {
|
|
472
|
+
return `"${value.replaceAll("\"", "\\\"")}"`;
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
export { jinjaEngine as t };
|
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitempl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Stepan Sotnikov",
|
|
6
|
-
"description": "
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": "^18.0.0 || >=20.0.0"
|
|
9
|
-
},
|
|
6
|
+
"description": "Vite-плагин: шаблоны для серверного рендеринга из Vue SFC",
|
|
10
7
|
"homepage": "https://github.com/sotnikovse/vitempl#readme",
|
|
11
8
|
"repository": {
|
|
12
9
|
"type": "git",
|
|
@@ -16,35 +13,52 @@
|
|
|
16
13
|
"url": "https://github.com/sotnikovse/vitempl/issues"
|
|
17
14
|
},
|
|
18
15
|
"keywords": [
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
16
|
+
"vite",
|
|
17
|
+
"vite-plugin",
|
|
18
|
+
"vue",
|
|
19
|
+
"template",
|
|
20
|
+
"engine",
|
|
21
|
+
"hbs",
|
|
22
|
+
"handlebars",
|
|
23
|
+
"askama",
|
|
24
|
+
"jinja",
|
|
25
|
+
"build-tool"
|
|
24
26
|
],
|
|
25
|
-
"packageManager": "pnpm@8.9.2",
|
|
26
27
|
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
30
|
+
},
|
|
27
31
|
"files": [
|
|
28
32
|
"dist"
|
|
29
33
|
],
|
|
30
|
-
"main": "./dist/index.cjs",
|
|
31
|
-
"module": "./dist/index.mjs",
|
|
32
|
-
"types": "./dist/index.d.ts",
|
|
33
34
|
"exports": {
|
|
34
|
-
".":
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
".": "./dist/index.js",
|
|
36
|
+
"./handlebars": "./dist/engines/handlebars.js",
|
|
37
|
+
"./askama": "./dist/engines/askama.js",
|
|
38
|
+
"./jinja": "./dist/engines/jinja.js"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"build": "tsc && tsdown",
|
|
43
|
+
"prepublishOnly": "npm run build",
|
|
44
|
+
"typecheck": "tsc",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": "^8.0.0",
|
|
49
|
+
"vue": "^3.5.0"
|
|
38
50
|
},
|
|
39
51
|
"dependencies": {
|
|
40
|
-
"@vue/compiler-
|
|
41
|
-
"
|
|
52
|
+
"@vue/compiler-core": "^3.5.43",
|
|
53
|
+
"@vue/shared": "^3.5.43"
|
|
42
54
|
},
|
|
43
55
|
"devDependencies": {
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
56
|
+
"@types/node": "^24.13.6",
|
|
57
|
+
"handlebars": "^4.7.9",
|
|
58
|
+
"tsdown": "^0.23.0",
|
|
59
|
+
"typescript": "~6.0.2",
|
|
60
|
+
"vite": "^8.3.0",
|
|
61
|
+
"vitest": "^5.0.1",
|
|
62
|
+
"vue": "^3.5.43"
|
|
49
63
|
}
|
|
50
|
-
}
|
|
64
|
+
}
|
package/dist/index.cjs
DELETED
package/dist/index.d.cts
DELETED
package/dist/index.d.mts
DELETED
package/dist/index.mjs
DELETED