ferrovue 0.1.0 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +277 -22
  2. package/dist/attrs.d.ts +67 -0
  3. package/dist/attrs.js +769 -0
  4. package/dist/cli.js +183 -14
  5. package/dist/client.d.ts +36 -0
  6. package/dist/client.js +93 -1
  7. package/dist/compiler.d.ts +20 -53
  8. package/dist/compiler.js +99 -3178
  9. package/dist/component.d.ts +6 -0
  10. package/dist/component.js +264 -0
  11. package/dist/context.d.ts +151 -0
  12. package/dist/context.js +102 -0
  13. package/dist/diff.d.ts +4 -0
  14. package/dist/diff.js +77 -0
  15. package/dist/emitter.d.ts +23 -0
  16. package/dist/emitter.js +58 -0
  17. package/dist/expr.d.ts +76 -0
  18. package/dist/expr.js +1305 -0
  19. package/dist/fixture.d.ts +38 -0
  20. package/dist/fixture.js +78 -0
  21. package/dist/i18n.d.ts +27 -0
  22. package/dist/i18n.js +175 -0
  23. package/dist/islands.d.ts +5 -0
  24. package/dist/islands.js +4 -0
  25. package/dist/lists.d.ts +19 -0
  26. package/dist/lists.js +253 -0
  27. package/dist/model.d.ts +269 -0
  28. package/dist/model.js +115 -0
  29. package/dist/parens.d.ts +51 -0
  30. package/dist/parens.js +239 -0
  31. package/dist/router.d.ts +17 -0
  32. package/dist/router.js +322 -0
  33. package/dist/rust.d.ts +17 -0
  34. package/dist/rust.js +523 -0
  35. package/dist/scoped.d.ts +24 -0
  36. package/dist/scoped.js +177 -0
  37. package/dist/script.d.ts +16 -0
  38. package/dist/script.js +332 -0
  39. package/dist/stores.d.ts +6 -0
  40. package/dist/stores.js +229 -0
  41. package/dist/template.d.ts +48 -0
  42. package/dist/template.js +1012 -0
  43. package/dist/testing.d.ts +2 -22
  44. package/dist/testing.js +11 -55
  45. package/dist/types.d.ts +1 -0
  46. package/dist/typescript.d.ts +31 -0
  47. package/dist/typescript.js +310 -0
  48. package/dist/vite.d.ts +23 -0
  49. package/dist/vite.js +157 -0
  50. package/package.json +30 -8
package/dist/attrs.js ADDED
@@ -0,0 +1,769 @@
1
+ /* Attributes as Vue's server renderer writes them: `class`, `style`, booleans, merged objects. */
2
+ import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, parseStringStyle, propsToAttrMap } from "@vue/shared";
3
+ import { fail, GenError, rustStr, STR } from "./model.js";
4
+ import { CONFIG_FILE, ctx } from "./context.js";
5
+ import { cond, describeTy, expr, known, meet, truthy, unquote } from "./expr.js";
6
+ import { atom, bare, condition, logical, not, receiver, strArg } from "./parens.js";
7
+ import { Emitter } from "./emitter.js";
8
+ import { isDollarAttrs } from "./scoped.js";
9
+ /** What a number written at run time is expected to take: most a page shows are shorter, and the
10
+ * reservation is an estimate. */
11
+ const NUMBER_BYTES = 6;
12
+ /** A string, a number or a boolean written escaped: now when it is known, by JavaScript itself. */
13
+ function display(e, v) {
14
+ if (v.ty.k === "str" && known(v) !== undefined)
15
+ e.lit(escapeHtml(unquote(v.code)));
16
+ else if ((v.ty.k === "int" || v.ty.k === "float") && v.num !== undefined)
17
+ e.lit(String(v.num));
18
+ else if (v.ty.k === "bool" && v.konst !== undefined)
19
+ e.lit(String(v.konst));
20
+ else if (v.ty.k === "str")
21
+ e.stmt(`fv::escape_into(out, ${strArg(v.code)});`);
22
+ else if (v.ty.k === "int" || v.ty.k === "float") {
23
+ e.stmt(`fv::${v.ty.k === "int" ? "push_int" : "push_number"}(out, ${bare(v.code)});`);
24
+ // Counted in the reservation, or a page of numbers outgrows it and is copied to a larger one.
25
+ e.expect(NUMBER_BYTES);
26
+ }
27
+ else if (v.ty.k === "bool")
28
+ e.stmt(`out.push_str(if ${condition(v.code)} { "true" } else { "false" });`);
29
+ else
30
+ return false;
31
+ return true;
32
+ }
33
+ /** `toDisplayString`, escaped. */
34
+ export function interpolate(e, v) {
35
+ if (display(e, v))
36
+ return;
37
+ switch (v.ty.k) {
38
+ case "undef":
39
+ return;
40
+ // `toDisplayString` of a query value: a string, nothing for `null`, an array as JSON.
41
+ case "query":
42
+ e.stmt(`${atom(v.code)}.write_display(out);`);
43
+ return;
44
+ case "opt":
45
+ e.open(`if let Some(v) = ${v.code}`);
46
+ interpolate(e, { code: "v", ty: v.ty.of });
47
+ e.close();
48
+ return;
49
+ default:
50
+ throw new GenError("only strings, numbers and booleans can be interpolated");
51
+ }
52
+ }
53
+ /** The value half of `key="value"`, escaped. */
54
+ export function attrValue(e, v) {
55
+ if (!display(e, v))
56
+ throw new GenError("an attribute value must be a string, a number or a boolean");
57
+ }
58
+ /** Vue's server renderer leaves out an attribute whose value is not a string, a number or a boolean
59
+ * (`isRenderableAttrValue`), but hydration then sets it, to `String(value)`, without reporting a
60
+ * mismatch: the page changes as it hydrates. So a value that may be anything else is refused. */
61
+ function renderable(s, key, v, n) {
62
+ const ty = v.ty.k === "opt" ? v.ty.of : v.ty;
63
+ if (ty.k === "str" || ty.k === "int" || ty.k === "float" || ty.k === "bool" || ty.k === "undef")
64
+ return;
65
+ const what = ty.k === "query" ? "a query value, which is an array when its key is repeated" : describeTy(ty);
66
+ const fix = ty.k === "query" ? 'narrow it to one string, as `typeof route.query.q === "string" ? route.query.q : ""`'
67
+ : ty.k === "list" ? 'join it into one string, as `.join(",")`'
68
+ : "bind a string, a number or a boolean";
69
+ fail(s.comp, `\`${key}\` is bound to ${what}: Vue's server renderer leaves the attribute out, and hydration then sets it, changing the page; ${fix}`, n);
70
+ }
71
+ /** `ssrRenderAttr(key, value)`: absent for null/undefined, `key="value"` for anything else. */
72
+ export function renderAttr(s, e, key, v, n) {
73
+ renderable(s, key, v, n);
74
+ if (v.ty.k === "undef")
75
+ return;
76
+ if (v.ty.k === "opt") {
77
+ e.open(`if let Some(v) = ${v.code}`);
78
+ renderAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
79
+ e.close();
80
+ return;
81
+ }
82
+ e.lit(` ${key}="`);
83
+ attrValue(e, v);
84
+ e.lit(`"`);
85
+ }
86
+ /** `ssrRenderDynamicAttr(key, value)`: as `renderAttr`, but a boolean attribute is present or
87
+ * absent, and an empty string renders the name alone. */
88
+ export function renderDynamicAttr(s, e, key, v, n) {
89
+ const name = propsToAttrMap[key] ?? key.toLowerCase();
90
+ if (!isSSRSafeAttrName(name))
91
+ fail(s.comp, `unsafe attribute name \`${name}\``, n);
92
+ renderable(s, key, v, n);
93
+ if (v.ty.k === "undef")
94
+ return;
95
+ const boolean = (t) => isBooleanAttr(name) || (name === "hidden" && (t.k === "bool" || t.k === "int" || t.k === "float"));
96
+ if (v.ty.k === "opt" && boolean(v.ty.of)) {
97
+ // Present, or truthy when it is not a string: one test, with nothing to bind.
98
+ e.open(`if ${v.ty.of.k === "str" ? `${atom(v.code)}.is_some()` : truthy(v)}`);
99
+ e.lit(` ${name}`);
100
+ e.close();
101
+ return;
102
+ }
103
+ if (v.ty.k === "opt") {
104
+ e.open(`if let Some(v) = ${v.code}`);
105
+ renderDynamicAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
106
+ e.close();
107
+ return;
108
+ }
109
+ if (boolean(v.ty)) {
110
+ const k = v.ty.k === "str" ? true : known(v);
111
+ if (k === true)
112
+ e.lit(` ${name}`);
113
+ else if (k === undefined) {
114
+ e.open(`if ${condition(truthy(v))}`);
115
+ e.lit(` ${name}`);
116
+ e.close();
117
+ }
118
+ return;
119
+ }
120
+ if (v.ty.k === "str" && known(v) === false) {
121
+ e.lit(` ${name}`);
122
+ return;
123
+ }
124
+ if (v.ty.k === "str" && known(v) === undefined) {
125
+ e.open(`if ${receiver(v.code)}.is_empty()`);
126
+ e.lit(` ${name}`);
127
+ e.close(" else {");
128
+ e.lit(` ${name}="`);
129
+ // Re-evaluated rather than bound: every string expression here is a borrow or a pure call.
130
+ attrValue(e, v);
131
+ e.lit(`"`);
132
+ e.close();
133
+ return;
134
+ }
135
+ e.lit(` ${name}="`);
136
+ attrValue(e, v);
137
+ e.lit(`"`);
138
+ }
139
+ /** `normalizeClass`'s view of a class binding, flattened: a string, an array of items, an object
140
+ * of `name: condition`, `cond && "name"`, `cond ? "a" : null`. Every item is trimmed and the empty
141
+ * ones dropped before they are joined with one space, which is what `class_into` does too, so an
142
+ * object's keys become items of their own. */
143
+ export function classItems(s, n) {
144
+ switch (n.type) {
145
+ case "StringLiteral": {
146
+ const t = n.value.trim();
147
+ return t ? [{ lit: t }] : [];
148
+ }
149
+ case "ArrayExpression":
150
+ return n.elements.flatMap((el) => (el ? classItems(s, el) : []));
151
+ case "ObjectExpression":
152
+ // A computed name may hold spaces, which Vue keeps between the names it joins: such an object
153
+ // is normalised whole, at run time, as Vue normalises it.
154
+ // Normalised whole, at run time, as a JavaScript object: when a name is computed (and may hold
155
+ // spaces, repeat another, or be an array index), or a literal name repeats or is an index —
156
+ // which a JavaScript object lists first, in numeric order.
157
+ const literalNames = n.properties
158
+ .filter((p) => p.type === "ObjectProperty" && !p.computed)
159
+ .map((p) => (p.key.type === "Identifier" ? p.key.name : String(p.key.value)));
160
+ const arrayIndex = (k) => /^(0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1;
161
+ if (n.properties.some((p) => p.type === "ObjectProperty" && p.computed) ||
162
+ literalNames.some(arrayIndex) ||
163
+ new Set(literalNames).size !== literalNames.length) {
164
+ const keys = [];
165
+ const entries = n.properties.map((p) => {
166
+ if (p.type !== "ObjectProperty")
167
+ fail(s.comp, "a class object holds `name: condition` pairs", p);
168
+ const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
169
+ if (key.ty.k !== "str")
170
+ fail(s.comp, "a computed class name is a string", p.key);
171
+ // Names are told apart, which two halves of surrogate pairs would not be.
172
+ for (const other of keys)
173
+ meet(s.comp, other, key, "a class object's names", p.key, "equal");
174
+ keys.push(key);
175
+ return `(${bare(cond(s, p.value))}, ${bare(key.code)})`;
176
+ });
177
+ return [{ code: `&*fv::class_object(&[${entries.join(", ")}])` }];
178
+ }
179
+ return n.properties.flatMap((p) => {
180
+ if (p.type !== "ObjectProperty")
181
+ fail(s.comp, "a class object holds `name: condition` pairs", p);
182
+ // A literal name is written as it is: an object's names are joined, not trimmed one by one.
183
+ let name;
184
+ if (!p.computed) {
185
+ const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
186
+ if (key !== key.trim() || !key)
187
+ fail(s.comp, `class name \`${key}\` has spaces around it`, p);
188
+ name = { lit: key };
189
+ }
190
+ else {
191
+ const k = expr(s, p.key);
192
+ if (k.ty.k !== "str")
193
+ fail(s.comp, "a computed class name is a string", p.key);
194
+ name = { code: `fv::js_trim(${strArg(k.code)})` };
195
+ }
196
+ const v = expr(s, p.value);
197
+ const on = known(v);
198
+ if (on !== undefined)
199
+ return on ? [name] : [];
200
+ const text = "lit" in name ? rustStr(name.lit) : name.code;
201
+ return [{ code: `if ${condition(truthy(v))} { ${bare(text)} } else { "" }` }];
202
+ });
203
+ case "LogicalExpression":
204
+ if (n.operator === "&&") {
205
+ return conditional(cond(s, n.left), classItems(s, n.right));
206
+ }
207
+ break;
208
+ case "ConditionalExpression":
209
+ if (n.consequent.type === "NullLiteral" || n.alternate.type === "NullLiteral") {
210
+ const branch = n.consequent.type === "NullLiteral" ? n.alternate : n.consequent;
211
+ const test = cond(s, n.test);
212
+ return conditional(n.consequent.type === "NullLiteral" ? not(test) : test, classItems(s, branch));
213
+ }
214
+ break;
215
+ }
216
+ const v = expr(s, n);
217
+ if (v.ty.k === "str")
218
+ return [{ code: v.code }];
219
+ if (v.ty.k === "opt" && v.ty.of.k === "str")
220
+ return [{ code: `${atom(v.code)}.unwrap_or("")` }];
221
+ if (v.ty.k === "undef")
222
+ return [];
223
+ return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
224
+ }
225
+ /** Class items that apply only when \`test\` holds: each one, or nothing, decided now if it can be. */
226
+ function conditional(test, items) {
227
+ if (test === "true" || test === "false")
228
+ return test === "true" ? items : [];
229
+ return items.map((it) => ({ code: `if ${condition(test)} { ${"lit" in it ? rustStr(it.lit) : bare(it.code)} } else { "" }` }));
230
+ }
231
+ /** `ssrRenderClass(value)`: normalised, then escaped. `after` says a class was already written. */
232
+ export function renderClass(s, e, n, after = false) {
233
+ const items = classItems(s, n);
234
+ // Literal items before the first run-time one are written now; the rest go to `class_into`,
235
+ // which decides at run time whether each needs a separator.
236
+ let wrote = after;
237
+ let i = 0;
238
+ for (; i < items.length; i++) {
239
+ const it = items[i];
240
+ if (!("lit" in it))
241
+ break;
242
+ e.lit((wrote ? " " : "") + escapeHtml(it.lit));
243
+ wrote = true;
244
+ }
245
+ if (i < items.length) {
246
+ const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
247
+ e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
248
+ }
249
+ }
250
+ export const IGNORED_PROPS = new Set(["", "key", "ref", "innerHTML", "textContent", "ref_key", "ref_for"]);
251
+ /** `ssrRenderAttrs(_attrs)` on the root, when no attribute reaches it at run time: the scope ids a
252
+ * parent handed it, already written as Vue writes them. */
253
+ function inheritedAttrs(s, e) {
254
+ if (s.attrs !== null)
255
+ e.stmt(`out.push_str(${s.attrs});`);
256
+ }
257
+ /** `ssrGetDirectiveProps(_ctx, dir)`: a custom directive's server props, which are none for a
258
+ * directive the configuration declares client-only. `false` when `n` is not such a call. */
259
+ export function directiveProps(s, n) {
260
+ if (n?.type !== "CallExpression" || n.callee.type !== "Identifier" || n.callee.name !== "_ssrGetDirectiveProps")
261
+ return false;
262
+ const dir = n.arguments[1];
263
+ // `$setup["vFocus"]` for an imported directive, `_directive_focus` for a registered one.
264
+ let name = null;
265
+ if (dir?.type === "MemberExpression") {
266
+ const local = dir.computed ? dir.property.value : dir.property.name;
267
+ name = hyphenate(local.replace(/^v(?=[A-Z])/, "")).replace(/^-/, "");
268
+ }
269
+ else if (dir?.type === "Identifier")
270
+ name = s.directives.get(dir.name) ?? null;
271
+ if (name === null || !ctx.clientDirectives.has(name)) {
272
+ fail(s.comp, `custom directive \`v-${name ?? "?"}\` may add attributes on the server; if it has no \`getSSRProps\`, list \`${name ?? "?"}\` in \`clientDirectives\` in ${CONFIG_FILE}`, n);
273
+ }
274
+ return true;
275
+ }
276
+ /** Whether `n` is the root's `_attrs`: what a parent passes, then the scope ids. */
277
+ export function isAttrs(n) {
278
+ return n?.type === "Identifier" && n.name === "_attrs";
279
+ }
280
+ /** Whether `n` is `$attrs` — `_ctx.$attrs`, or a `useAttrs()` binding — which a component whose
281
+ * root is another's would find holding scope ids as well. */
282
+ export function dollarAttrs(s, n) {
283
+ if (!isDollarAttrs(n, s.attrsBindings))
284
+ return false;
285
+ if (s.comp.idsInAttrs) {
286
+ fail(s.comp, `\`$attrs\` in ${s.comp.name}, the root of a component that may be handed scope ids: it would hold them as attributes too`, n);
287
+ }
288
+ return true;
289
+ }
290
+ /** The parts `_mergeProps(…)` merges, or the one object that is not merged. */
291
+ export function mergedParts(n) {
292
+ return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_mergeProps" ? n.arguments : [n];
293
+ }
294
+ /** `ssrRenderAttrs(obj)`, key by key in the object's order, and the inherited scope ids where the
295
+ * root's `_attrs` is merged in. When a parent may pass attributes, which only the run time knows,
296
+ * `$attrs` and the root's `_attrs` are merged with the element's own by `fv::attrs_into` — but only
297
+ * when some were passed: with none, the element is written as it is without. */
298
+ export function renderAttrs(s, e, n) {
299
+ const parts = mergedParts(n);
300
+ const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
301
+ if (!parts.some(passed)) {
302
+ staticAttrs(s, e, n);
303
+ return;
304
+ }
305
+ e.open(`if ${s.fallthrough}.is_empty()`);
306
+ const at = e.lines.length;
307
+ staticAttrs(s, e, n);
308
+ e.flush();
309
+ // Nothing to write when nothing is passed: only the other branch is left.
310
+ if (e.lines.length === at)
311
+ e.replace(at - 1, `if ${s.fallthrough}.is_empty()`, `if !${s.fallthrough}.is_empty()`);
312
+ else
313
+ e.close(" else {");
314
+ // `_attrs` or `$attrs` alone is written as it is, with no `mergeProps` to normalise it.
315
+ if (parts.length === 1 && parts[0] === n) {
316
+ e.stmt(`fv::passed_attrs_into(out, ${s.fallthrough}.list(), ${isAttrs(n) && s.attrs !== null ? s.attrs : '""'});`);
317
+ e.close();
318
+ return;
319
+ }
320
+ const sources = [];
321
+ let idsAt = -1;
322
+ for (const p of parts) {
323
+ if (directiveProps(s, p))
324
+ continue;
325
+ if (isAttrs(p))
326
+ idsAt = sources.length;
327
+ if (passed(p))
328
+ sources.push(`${s.fallthrough}.list()`);
329
+ else if (isAttrs(p))
330
+ sources.push("&[]");
331
+ else if (p.type === "ObjectExpression")
332
+ sources.push(`&[${attrEntries(s, p, "own").join(", ")}]`);
333
+ else
334
+ fail(s.comp, "attributes must be an object literal", p);
335
+ }
336
+ const ids = idsAt >= 0 && s.attrs !== null ? s.attrs : '""';
337
+ e.stmt(`fv::attrs_into(out, &[${sources.join(", ")}], ${idsAt >= 0 ? idsAt : sources.length}, ${ids});`);
338
+ e.close();
339
+ }
340
+ /** `ssrRenderAttrs(obj)` with nothing passed at run time: `$attrs` empty, `_attrs` the scope ids. */
341
+ function staticAttrs(s, e, n) {
342
+ if (isAttrs(n)) {
343
+ inheritedAttrs(s, e);
344
+ return;
345
+ }
346
+ if (dollarAttrs(s, n) || directiveProps(s, n))
347
+ return;
348
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_mergeProps") {
349
+ const merged = mergeProps(s, n);
350
+ const at = n.arguments.findIndex(isAttrs);
351
+ if (at < 0) {
352
+ objectAttrs(s, e, merged, true);
353
+ return;
354
+ }
355
+ // The inherited ids take their place among the keys: after those given before `_attrs`, before
356
+ // those first given after it, such as `v-show`'s `style` when there is no other.
357
+ const before = new Set(mergeProps(s, { ...n, arguments: n.arguments.slice(0, at) }).properties.map((p) => p.key.name ?? p.key.value));
358
+ const part = (early) => ({ ...merged, properties: merged.properties.filter((p) => before.has(p.key.name ?? p.key.value) === early) });
359
+ objectAttrs(s, e, part(true), true);
360
+ inheritedAttrs(s, e);
361
+ objectAttrs(s, e, part(false), true);
362
+ return;
363
+ }
364
+ objectAttrs(s, e, n, false);
365
+ }
366
+ /** The keys of one attribute object. `merged` says `mergeProps` made it, which normalises the class
367
+ * and the style first: a class that is `undefined` is left out, and a style given as text is parsed
368
+ * and written back. */
369
+ function objectAttrs(s, e, n, merged) {
370
+ if (n.type !== "ObjectExpression")
371
+ fail(s.comp, "attributes must be an object literal", n);
372
+ for (const p of n.properties) {
373
+ if (p.type !== "ObjectProperty" || p.computed)
374
+ fail(s.comp, "attribute objects hold plain keys", p);
375
+ const raw = p.key.type === "Identifier" ? p.key.name : p.key.value;
376
+ if (IGNORED_PROPS.has(raw) || /^on[^a-z]/.test(raw) || raw.startsWith("."))
377
+ continue;
378
+ // Vue strips the `^` attribute-binding prefix before it looks at the name.
379
+ const key = raw.startsWith("^") ? raw.slice(1) : raw;
380
+ if (key === "className") {
381
+ fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", p);
382
+ }
383
+ else if (key === "class") {
384
+ const present = merged ? classPresent(s, p.value) : "true";
385
+ if (present === "false")
386
+ continue;
387
+ if (present !== "true")
388
+ e.open(`if ${condition(present)}`);
389
+ e.lit(` class="`);
390
+ renderClass(s, e, p.value);
391
+ e.lit(`"`);
392
+ if (present !== "true")
393
+ e.close();
394
+ }
395
+ else if (key === "style") {
396
+ e.lit(` style="`);
397
+ if (merged)
398
+ mergedStyle(s, e, p.value);
399
+ else
400
+ renderStyle(s, e, p.value);
401
+ e.lit(`"`);
402
+ }
403
+ else {
404
+ renderDynamicAttr(s, e, key, expr(s, p.value), p);
405
+ }
406
+ }
407
+ }
408
+ /** When `mergeProps` gives an element a `class` at all: unless every value it merges is
409
+ * `undefined`, which `ret.class !== undefined` leaves out. */
410
+ function classPresent(s, n) {
411
+ if (n.fvMerged)
412
+ return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");
413
+ switch (n.type) {
414
+ case "StringLiteral":
415
+ case "NullLiteral":
416
+ case "ArrayExpression":
417
+ case "ObjectExpression":
418
+ return "true";
419
+ case "LogicalExpression":
420
+ if (n.operator === "&&")
421
+ return logical(not(cond(s, n.left)), "||", classPresent(s, n.right));
422
+ break;
423
+ case "ConditionalExpression":
424
+ if (n.consequent.type === "NullLiteral")
425
+ return logical(cond(s, n.test), "||", classPresent(s, n.alternate));
426
+ if (n.alternate.type === "NullLiteral")
427
+ return logical(not(cond(s, n.test)), "||", classPresent(s, n.consequent));
428
+ break;
429
+ }
430
+ const v = expr(s, n);
431
+ if (v.ty.k === "undef")
432
+ return "false";
433
+ if (v.ty.k === "opt")
434
+ return `${atom(v.code)}.is_some()`;
435
+ return "true";
436
+ }
437
+ /** A style `mergeProps` merged: normalised, so that text is parsed into properties — by Vue's own
438
+ * parser now when it is literal, by `fv::style_text_into` at run time when it is not. */
439
+ function mergedStyle(s, e, n) {
440
+ if (n.type === "StringLiteral") {
441
+ renderStyle(s, e, { type: "ArrayExpression", elements: [n] });
442
+ return;
443
+ }
444
+ if (n.fvMerged || ["ObjectExpression", "ArrayExpression", "ConditionalExpression", "LogicalExpression", "NullLiteral"].includes(n.type)) {
445
+ renderStyle(s, e, n);
446
+ return;
447
+ }
448
+ const v = expr(s, n);
449
+ if (v.ty.k === "str")
450
+ e.stmt(`fv::style_text_into(out, ${strArg(v.code)});`);
451
+ else if (v.ty.k === "opt" && v.ty.of.k === "str") {
452
+ e.open(`if let Some(v) = ${v.code}`);
453
+ e.stmt("fv::style_text_into(out, v);");
454
+ e.close();
455
+ }
456
+ else if (v.ty.k !== "undef")
457
+ fail(s.comp, "a style binding is a string, an object or an array", n);
458
+ }
459
+ /** Whether a class value could equal, as a string, the class merged before it: a string. */
460
+ function maybeEqual(s, n) {
461
+ if (n.type === "StringLiteral")
462
+ return true;
463
+ if (n.fvMerged || ["ArrayExpression", "ObjectExpression", "NullLiteral", "LogicalExpression", "ConditionalExpression"].includes(n.type))
464
+ return false;
465
+ const v = expr(s, n);
466
+ return v.ty.k === "str" || (v.ty.k === "opt" && v.ty.of.k === "str");
467
+ }
468
+ /** A class value's names, known now: a literal, or literals merged. */
469
+ function literalClass(n) {
470
+ if (n.type === "StringLiteral")
471
+ return n.value.trim();
472
+ if (!n.fvMerged)
473
+ return null;
474
+ const parts = n.elements.map(literalClass);
475
+ return parts.includes(null) ? null : parts.filter(Boolean).join(" ");
476
+ }
477
+ /** `mergeProps(a, _attrs, b, …)` as one object literal, without `_attrs` and `$attrs`, which hold
478
+ * what only the run time knows. Of the rest, `class` and `style` values are merged as an array —
479
+ * unless a class equals the one so far, which Vue then keeps once — and any other key keeps the
480
+ * place it first had with the last value given. */
481
+ export function mergeProps(s, n) {
482
+ const merged = new Map();
483
+ for (const a of n.arguments) {
484
+ if (isAttrs(a) || dollarAttrs(s, a))
485
+ continue;
486
+ if (directiveProps(s, a))
487
+ continue;
488
+ if (a.type !== "ObjectExpression")
489
+ fail(s.comp, "`_mergeProps` of object literals is supported", a);
490
+ for (const p of a.properties) {
491
+ if (p.type !== "ObjectProperty" || p.computed)
492
+ fail(s.comp, "attribute objects hold plain keys", p);
493
+ const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
494
+ const had = merged.get(key);
495
+ if (had && key === "class" && maybeEqual(s, p.value)) {
496
+ // `ret.class !== toMerge.class`: a string equal to the class so far is not added again.
497
+ const before = literalClass(had.value);
498
+ if (before === null || p.value.type !== "StringLiteral") {
499
+ fail(s.comp, "a class merged with a string that may equal the class before it, which Vue then writes once: bind an array", p);
500
+ }
501
+ if (before !== p.value.value)
502
+ merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
503
+ }
504
+ else if (had && (key === "class" || key === "style")) {
505
+ merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
506
+ }
507
+ else if (had) {
508
+ // `Map` keeps a key where it was first set, as a JavaScript object does.
509
+ merged.set(key, { ...had, value: p.value });
510
+ }
511
+ else
512
+ merged.set(key, p);
513
+ }
514
+ }
515
+ return { type: "ObjectExpression", properties: [...merged.values()] };
516
+ }
517
+ /** The attributes of an object literal as `fv::Attr` entries, `("name", value)`, for a merge at run
518
+ * time: given to a child component (`vnode`), whose virtual node normalises a class array or object
519
+ * to a string, or an element's own (`own`), which `mergeProps` keeps as an object. */
520
+ export function attrEntries(s, obj, side) {
521
+ const out = [];
522
+ for (const p of obj.properties) {
523
+ if (p.type !== "ObjectProperty" || p.computed)
524
+ fail(s.comp, "attribute objects hold plain keys", p);
525
+ const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
526
+ if (IGNORED_PROPS.has(key) || /^on[^a-z]/.test(key) || key.startsWith("."))
527
+ continue;
528
+ out.push(`(${rustStr(key)}, ${attrOf(s, key, p.value, side)})`);
529
+ }
530
+ return out;
531
+ }
532
+ /** One attribute's value as an `fv::Attr`. */
533
+ export function attrOf(s, key, n, side) {
534
+ if (key === "className")
535
+ fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", n);
536
+ if (key === "class")
537
+ return classAttr(s, n, side);
538
+ if (key === "style")
539
+ return styleAttr(s, n);
540
+ if (n.type === "NullLiteral")
541
+ return "fv::Attr::Undefined";
542
+ return valueAttr(s, expr(s, n), n);
543
+ }
544
+ /** A string, a number, a boolean or `undefined` as an `fv::Attr`. */
545
+ function valueAttr(s, v, n) {
546
+ switch (v.ty.k) {
547
+ case "str":
548
+ return `fv::Attr::str(${strArg(v.code)})`;
549
+ case "int":
550
+ return `fv::Attr::Int(${bare(v.code)})`;
551
+ case "float":
552
+ return `fv::Attr::Float(${bare(v.code)})`;
553
+ case "bool":
554
+ return `fv::Attr::Bool(${bare(v.code)})`;
555
+ case "undef":
556
+ return "fv::Attr::Undefined";
557
+ case "opt": {
558
+ const of = v.ty.of.k;
559
+ const make = of === "str" ? "fv::Attr::str" : of === "int" ? "fv::Attr::Int" : of === "float" ? "fv::Attr::Float" : of === "bool" ? "fv::Attr::Bool" : null;
560
+ if (make !== null)
561
+ return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
562
+ }
563
+ }
564
+ return fail(s.comp, "an attribute that may fall through is a string, a number or a boolean", n);
565
+ }
566
+ /** A class as an `fv::Attr`: a string as it is, an array or an object by the names it normalises
567
+ * to, `cond && x` and `cond ? x : y` decided at run time. */
568
+ function classAttr(s, n, side) {
569
+ switch (n.type) {
570
+ case "StringLiteral":
571
+ return `fv::Attr::str(${rustStr(n.value)})`;
572
+ // `null`, like `false`, joins as nothing and equals no class: an empty string does the same.
573
+ case "NullLiteral":
574
+ return 'fv::Attr::str("")';
575
+ case "ArrayExpression":
576
+ case "ObjectExpression": {
577
+ const items = classItems(s, n);
578
+ const names = items.every((it) => "lit" in it)
579
+ ? rustStr(items.map((it) => ("lit" in it ? it.lit : "")).join(" "))
580
+ : `fv::class_names(&[${items.map((it) => ("lit" in it ? rustStr(it.lit) : it.code)).join(", ")}])`;
581
+ const literal = names.startsWith('"');
582
+ if (side === "vnode")
583
+ return literal ? `fv::Attr::str(${names})` : `fv::Attr::from(${names})`;
584
+ return `fv::Attr::Names(${literal ? `std::borrow::Cow::Borrowed(${names})` : `${names}.into()`})`;
585
+ }
586
+ case "LogicalExpression":
587
+ if (n.operator === "&&")
588
+ return `if ${condition(cond(s, n.left))} { ${classAttr(s, n.right, side)} } else { fv::Attr::str("") }`;
589
+ break;
590
+ case "ConditionalExpression":
591
+ return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
592
+ }
593
+ const v = expr(s, n);
594
+ if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
595
+ return valueAttr(s, v, n);
596
+ return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
597
+ }
598
+ /** A style as an `fv::Attr`: text as it is, objects by their properties, arrays merged, `cond && x`
599
+ * and `cond ? x : y` decided at run time. */
600
+ function styleAttr(s, n) {
601
+ switch (n.type) {
602
+ case "StringLiteral":
603
+ return `fv::Attr::str(${rustStr(n.value)})`;
604
+ case "NullLiteral":
605
+ return "fv::Attr::Undefined";
606
+ case "ObjectExpression": {
607
+ const entries = n.properties.map((p) => {
608
+ if (p.type !== "ObjectProperty" || p.computed)
609
+ fail(s.comp, "a style object holds plain `property: value` pairs", p);
610
+ const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
611
+ if (/^\d+$/.test(key) || key.startsWith(":"))
612
+ fail(s.comp, `style property \`${key}\``, p);
613
+ return `(${rustStr(key)}, ${valueAttr(s, expr(s, p.value), p.value)})`;
614
+ });
615
+ return `fv::Attr::style([${entries.join(", ")}])`;
616
+ }
617
+ case "ArrayExpression":
618
+ return `fv::Attr::styles([${n.elements.map((el) => (el ? styleAttr(s, el) : "fv::Attr::Undefined")).join(", ")}])`;
619
+ case "LogicalExpression":
620
+ if (n.operator === "&&")
621
+ return `if ${condition(cond(s, n.left))} { ${styleAttr(s, n.right)} } else { fv::Attr::Undefined }`;
622
+ break;
623
+ case "ConditionalExpression":
624
+ return `if ${condition(cond(s, n.test))} { ${styleAttr(s, n.consequent)} } else { ${styleAttr(s, n.alternate)} }`;
625
+ }
626
+ const v = expr(s, n);
627
+ if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
628
+ return valueAttr(s, v, n);
629
+ return fail(s.comp, "a style binding is a string, an object or an array", n);
630
+ }
631
+ /** The objects a style binding merges, in order: an object literal, a static `style` the compiler
632
+ * parsed, `v-show`'s `cond ? null : { display: "none" }`, and arrays of those. */
633
+ export function styleItems(s, n, when) {
634
+ // A condition known now: an object that never applies is left out, one that always does is not
635
+ // conditional.
636
+ if (when === "false")
637
+ return [];
638
+ if (when === "true")
639
+ when = null;
640
+ const both = (a, b) => (a === null ? b : logical(a, "&&", b));
641
+ switch (n.type) {
642
+ case "NullLiteral":
643
+ return [];
644
+ case "ArrayExpression":
645
+ return n.elements.flatMap((el) => (el ? styleItems(s, el, when) : []));
646
+ case "StringLiteral":
647
+ // Parsed as `normalizeStyle` parses a string inside an array, by Vue's own function.
648
+ return [{ cond: when, entries: Object.entries(parseStringStyle(n.value)).map(([key, v]) => ({ key, css: key, value: { type: "StringLiteral", value: v } })) }];
649
+ case "ConditionalExpression": {
650
+ const t = cond(s, n.test);
651
+ return [...styleItems(s, n.consequent, both(when, t)), ...styleItems(s, n.alternate, both(when, not(t)))];
652
+ }
653
+ case "LogicalExpression":
654
+ if (n.operator === "&&")
655
+ return styleItems(s, n.right, both(when, cond(s, n.left)));
656
+ break;
657
+ case "ObjectExpression":
658
+ return [{
659
+ cond: when,
660
+ entries: n.properties.map((p) => {
661
+ if (p.type !== "ObjectProperty" || p.computed)
662
+ fail(s.comp, "a style object holds plain `property: value` pairs", p);
663
+ const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
664
+ // An integer-like key comes first in a JavaScript object whatever the order written.
665
+ if (/^\d+$/.test(key) || key.startsWith(":"))
666
+ fail(s.comp, `style property \`${key}\``, p);
667
+ return { key, css: key.startsWith("--") ? key : hyphenate(key), value: p.value };
668
+ }),
669
+ }];
670
+ }
671
+ return fail(s.comp, "a style binding is an object, an array of objects, or a string on its own", n);
672
+ }
673
+ /** `ssrRenderStyle(value)`: a string as it is, or the objects merged — each property where it first
674
+ * appears, with the last value given — and written `name:value;`, all escaped. */
675
+ export function renderStyle(s, e, n) {
676
+ // A string on its own is written as it is, with no normalising.
677
+ if (n.type === "StringLiteral") {
678
+ e.lit(escapeHtml(n.value));
679
+ return;
680
+ }
681
+ if (n.type !== "ObjectExpression" && n.type !== "ArrayExpression" && n.type !== "ConditionalExpression" && n.type !== "LogicalExpression" && n.type !== "NullLiteral") {
682
+ const v = expr(s, n);
683
+ if (v.ty.k === "str")
684
+ e.stmt(`fv::escape_into(out, ${strArg(v.code)});`);
685
+ else if (v.ty.k === "opt" && v.ty.of.k === "str") {
686
+ e.open(`if let Some(v) = ${v.code}`);
687
+ e.stmt("fv::escape_into(out, v);");
688
+ e.close();
689
+ }
690
+ else if (v.ty.k !== "undef")
691
+ fail(s.comp, "a style binding is a string, an object or an array", n);
692
+ return;
693
+ }
694
+ const items = styleItems(s, n, null);
695
+ // Each property, where it first appears, with every place that sets it.
696
+ const order = [];
697
+ const sets = new Map();
698
+ for (const it of items) {
699
+ for (const en of it.entries) {
700
+ if (!sets.has(en.key)) {
701
+ order.push(en.key);
702
+ sets.set(en.key, []);
703
+ }
704
+ sets.get(en.key).push({ cond: it.cond, css: en.css, value: en.value });
705
+ }
706
+ }
707
+ const write = (css, value) => {
708
+ // A literal is stringified now, by JavaScript itself: `0.5`, `"1px"`.
709
+ if (value.type === "StringLiteral" || value.type === "NumericLiteral") {
710
+ e.lit(escapeHtml(`${css}:${String(value.value)};`));
711
+ return;
712
+ }
713
+ const v = expr(s, value);
714
+ const one = (w) => {
715
+ if (w.ty.k === "str" || w.ty.k === "int" || w.ty.k === "float") {
716
+ e.lit(`${escapeHtml(css)}:`);
717
+ display(e, w);
718
+ e.lit(";");
719
+ }
720
+ else if (w.ty.k === "opt") {
721
+ e.open(`if let Some(v) = ${w.code}`);
722
+ one({ code: "v", ty: w.ty.of });
723
+ e.close();
724
+ }
725
+ // A boolean or `undefined` writes nothing — and, set later, removes what was set before.
726
+ };
727
+ one(v);
728
+ };
729
+ // A property's place is where an object that is present first sets it. When that object is
730
+ // conditional and the property is set again after another one first appears, the order would be
731
+ // decided at run time.
732
+ const flat = items.flatMap((it, i) => it.entries.map((en) => ({ key: en.key, item: i, cond: it.cond })));
733
+ for (const key of order) {
734
+ const at = flat.filter((f) => f.key === key);
735
+ if (at.length < 2 || at[0].cond === null)
736
+ continue;
737
+ const first = flat.indexOf(at[0]);
738
+ const again = flat.indexOf(at[at.length - 1]);
739
+ const between = flat.slice(first + 1, again).some((f) => order.indexOf(f.key) > order.indexOf(key));
740
+ if (between)
741
+ fail(s.comp, `the place of style property \`${key}\` would depend on a condition; set it unconditionally first`, n);
742
+ }
743
+ for (const key of order) {
744
+ // The last place that sets it wins: walked back to the first that always applies.
745
+ const chain = [];
746
+ for (const set of sets.get(key).toReversed()) {
747
+ chain.push(set);
748
+ if (set.cond === null)
749
+ break;
750
+ }
751
+ if (chain.length === 1 && chain[0].cond === null) {
752
+ write(chain[0].css, chain[0].value);
753
+ continue;
754
+ }
755
+ chain.forEach((set, i) => {
756
+ if (set.cond === null) {
757
+ e.close(" else {");
758
+ }
759
+ else if (i === 0) {
760
+ e.open(`if ${condition(set.cond)}`);
761
+ }
762
+ else {
763
+ e.close(` else if ${condition(set.cond)} {`);
764
+ }
765
+ write(set.css, set.value);
766
+ });
767
+ e.close();
768
+ }
769
+ }