ferrovue 0.3.0 → 0.5.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/README.md +225 -42
- package/dist/attrs.d.ts +3 -53
- package/dist/attrs.js +41 -469
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +221 -0
- package/dist/children.d.ts +16 -0
- package/dist/children.js +286 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +31 -22
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -14
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +31 -92
- package/dist/component.d.ts +3 -1
- package/dist/component.js +123 -104
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +29 -67
- package/dist/context.js +13 -38
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/diff.d.ts +0 -3
- package/dist/diff.js +0 -9
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -70
- package/dist/expr.js +142 -934
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +4 -10
- package/dist/fixture.js +38 -27
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/islands.d.ts +1 -0
- package/dist/islands.js +1 -0
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +34 -60
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +168 -0
- package/dist/model.d.ts +46 -144
- package/dist/model.js +150 -18
- package/dist/narrowing.d.ts +27 -0
- package/dist/narrowing.js +157 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +88 -31
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +52 -91
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/plugins/scoped.js +125 -0
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +379 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +10 -0
- package/dist/routes.js +6 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +56 -174
- package/dist/script.d.ts +3 -6
- package/dist/script.js +127 -140
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +1 -42
- package/dist/template.js +25 -749
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +4 -10
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +2 -14
- package/dist/typescript.js +53 -59
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +11 -32
- package/package.json +15 -6
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -24
- package/dist/scoped.js +0 -177
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -229
package/dist/attrs.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { fail, GenError, rustStr, STR } from "./model.js";
|
|
1
|
+
import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
|
|
2
|
+
import { fail, nothing, GenError, rustStr } from "./model.js";
|
|
4
3
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { describeTy, expr } from "./expr.js";
|
|
5
|
+
import { known, truthy } from "./narrowing.js";
|
|
6
|
+
import { asCow, isTemporary, unquote } from "./strings.js";
|
|
7
|
+
import { atom, bare, condition, receiver, strArg } from "./parens.js";
|
|
7
8
|
import { Emitter } from "./emitter.js";
|
|
8
|
-
import { isDollarAttrs } from "./
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { isDollarAttrs } from "./fallthrough.js";
|
|
10
|
+
import { claim } from "./plugin.js";
|
|
11
|
+
import { classAttr, classPresent, literalClass, maybeEqual, renderClass } from "./classes.js";
|
|
12
|
+
import { mergedStyle, renderStyle, styleAttr } from "./styles.js";
|
|
11
13
|
const NUMBER_BYTES = 6;
|
|
12
|
-
|
|
13
|
-
function display(e, v) {
|
|
14
|
+
export function display(e, v) {
|
|
14
15
|
if (v.ty.k === "str" && known(v) !== undefined)
|
|
15
16
|
e.lit(escapeHtml(unquote(v.code)));
|
|
16
17
|
else if ((v.ty.k === "int" || v.ty.k === "float") && v.num !== undefined)
|
|
@@ -21,7 +22,6 @@ function display(e, v) {
|
|
|
21
22
|
e.stmt(`fv::escape_into(out, ${strArg(v.code)});`);
|
|
22
23
|
else if (v.ty.k === "int" || v.ty.k === "float") {
|
|
23
24
|
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
25
|
e.expect(NUMBER_BYTES);
|
|
26
26
|
}
|
|
27
27
|
else if (v.ty.k === "bool")
|
|
@@ -30,48 +30,40 @@ function display(e, v) {
|
|
|
30
30
|
return false;
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
export function interpolate(e, v) {
|
|
33
|
+
export function interpolate(s, e, v, n) {
|
|
35
34
|
if (display(e, v))
|
|
36
35
|
return;
|
|
37
36
|
switch (v.ty.k) {
|
|
38
37
|
case "undef":
|
|
39
|
-
|
|
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);`);
|
|
38
|
+
case "null":
|
|
43
39
|
return;
|
|
44
40
|
case "opt":
|
|
45
41
|
e.open(`if let Some(v) = ${v.code}`);
|
|
46
|
-
interpolate(e, { code: "v", ty: v.ty.of });
|
|
42
|
+
interpolate(s, e, { code: "v", ty: v.ty.of }, n);
|
|
47
43
|
e.close();
|
|
48
44
|
return;
|
|
49
45
|
default:
|
|
50
|
-
|
|
46
|
+
if (ctx.plugins.some((p) => p.values?.interpolate?.(e, v)))
|
|
47
|
+
return;
|
|
48
|
+
fail(s.comp, "FV0401", `\`{{ }}\` of ${describeTy(v.ty)}: only strings, numbers and booleans can be interpolated`, n);
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
|
-
/** The value half of `key="value"`, escaped. */
|
|
54
51
|
export function attrValue(e, v) {
|
|
55
52
|
if (!display(e, v))
|
|
56
|
-
throw new GenError("an attribute value must be a string, a number or a boolean");
|
|
53
|
+
throw new GenError("FV0402", "an attribute value must be a string, a number or a boolean");
|
|
57
54
|
}
|
|
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
55
|
function renderable(s, key, v, n) {
|
|
62
56
|
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
|
|
57
|
+
if (ty.k === "str" || ty.k === "int" || ty.k === "float" || ty.k === "bool" || nothing(ty))
|
|
64
58
|
return;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
59
|
+
const own = claim((p) => p.values?.unbindable?.(ty));
|
|
60
|
+
const what = own?.what ?? describeTy(ty);
|
|
61
|
+
const fix = own?.fix ?? (ty.k === "list" ? 'join it into one string, as `.join(",")`' : "bind a string, a number or a boolean");
|
|
62
|
+
fail(s.comp, "FV0403", `\`${key}\` is bound to ${what}: Vue's server renderer leaves the attribute out, and hydration then sets it, changing the page; ${fix}`, n);
|
|
70
63
|
}
|
|
71
|
-
/** `ssrRenderAttr(key, value)`: absent for null/undefined, `key="value"` for anything else. */
|
|
72
64
|
export function renderAttr(s, e, key, v, n) {
|
|
73
65
|
renderable(s, key, v, n);
|
|
74
|
-
if (v.ty
|
|
66
|
+
if (nothing(v.ty))
|
|
75
67
|
return;
|
|
76
68
|
if (v.ty.k === "opt") {
|
|
77
69
|
e.open(`if let Some(v) = ${v.code}`);
|
|
@@ -83,18 +75,15 @@ export function renderAttr(s, e, key, v, n) {
|
|
|
83
75
|
attrValue(e, v);
|
|
84
76
|
e.lit(`"`);
|
|
85
77
|
}
|
|
86
|
-
/** `ssrRenderDynamicAttr(key, value)`: as `renderAttr`, but a boolean attribute is present or
|
|
87
|
-
* absent, and an empty string renders the name alone. */
|
|
88
78
|
export function renderDynamicAttr(s, e, key, v, n) {
|
|
89
79
|
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
90
80
|
if (!isSSRSafeAttrName(name))
|
|
91
|
-
fail(s.comp, `unsafe attribute name \`${name}\``, n);
|
|
81
|
+
fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, n);
|
|
92
82
|
renderable(s, key, v, n);
|
|
93
|
-
if (v.ty
|
|
83
|
+
if (nothing(v.ty))
|
|
94
84
|
return;
|
|
95
85
|
const boolean = (t) => isBooleanAttr(name) || (name === "hidden" && (t.k === "bool" || t.k === "int" || t.k === "float"));
|
|
96
86
|
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
87
|
e.open(`if ${v.ty.of.k === "str" ? `${atom(v.code)}.is_some()` : truthy(v)}`);
|
|
99
88
|
e.lit(` ${name}`);
|
|
100
89
|
e.close();
|
|
@@ -126,7 +115,6 @@ export function renderDynamicAttr(s, e, key, v, n) {
|
|
|
126
115
|
e.lit(` ${name}`);
|
|
127
116
|
e.close(" else {");
|
|
128
117
|
e.lit(` ${name}="`);
|
|
129
|
-
// Re-evaluated rather than bound: every string expression here is a borrow or a pure call.
|
|
130
118
|
attrValue(e, v);
|
|
131
119
|
e.lit(`"`);
|
|
132
120
|
e.close();
|
|
@@ -136,131 +124,15 @@ export function renderDynamicAttr(s, e, key, v, n) {
|
|
|
136
124
|
attrValue(e, v);
|
|
137
125
|
e.lit(`"`);
|
|
138
126
|
}
|
|
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
127
|
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
128
|
function inheritedAttrs(s, e) {
|
|
254
129
|
if (s.attrs !== null)
|
|
255
130
|
e.stmt(`out.push_str(${s.attrs});`);
|
|
256
131
|
}
|
|
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
132
|
export function directiveProps(s, n) {
|
|
260
133
|
if (n?.type !== "CallExpression" || n.callee.type !== "Identifier" || n.callee.name !== "_ssrGetDirectiveProps")
|
|
261
134
|
return false;
|
|
262
135
|
const dir = n.arguments[1];
|
|
263
|
-
// `$setup["vFocus"]` for an imported directive, `_directive_focus` for a registered one.
|
|
264
136
|
let name = null;
|
|
265
137
|
if (dir?.type === "MemberExpression") {
|
|
266
138
|
const local = dir.computed ? dir.property.value : dir.property.name;
|
|
@@ -269,32 +141,24 @@ export function directiveProps(s, n) {
|
|
|
269
141
|
else if (dir?.type === "Identifier")
|
|
270
142
|
name = s.directives.get(dir.name) ?? null;
|
|
271
143
|
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);
|
|
144
|
+
fail(s.comp, "FV0405", `custom directive \`v-${name ?? "?"}\` may add attributes on the server; if it has no \`getSSRProps\`, list \`${name ?? "?"}\` in \`clientDirectives\` in ${CONFIG_FILE}`, n);
|
|
273
145
|
}
|
|
274
146
|
return true;
|
|
275
147
|
}
|
|
276
|
-
/** Whether `n` is the root's `_attrs`: what a parent passes, then the scope ids. */
|
|
277
148
|
export function isAttrs(n) {
|
|
278
149
|
return n?.type === "Identifier" && n.name === "_attrs";
|
|
279
150
|
}
|
|
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
151
|
export function dollarAttrs(s, n) {
|
|
283
152
|
if (!isDollarAttrs(n, s.attrsBindings))
|
|
284
153
|
return false;
|
|
285
154
|
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);
|
|
155
|
+
fail(s.comp, "FV0406", `\`$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
156
|
}
|
|
288
157
|
return true;
|
|
289
158
|
}
|
|
290
|
-
/** The parts `_mergeProps(…)` merges, or the one object that is not merged. */
|
|
291
159
|
export function mergedParts(n) {
|
|
292
160
|
return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_mergeProps" ? n.arguments : [n];
|
|
293
161
|
}
|
|
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
162
|
export function renderAttrs(s, e, n) {
|
|
299
163
|
const parts = mergedParts(n);
|
|
300
164
|
const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
@@ -306,12 +170,10 @@ export function renderAttrs(s, e, n) {
|
|
|
306
170
|
const at = e.lines.length;
|
|
307
171
|
staticAttrs(s, e, n);
|
|
308
172
|
e.flush();
|
|
309
|
-
// Nothing to write when nothing is passed: only the other branch is left.
|
|
310
173
|
if (e.lines.length === at)
|
|
311
174
|
e.replace(at - 1, `if ${s.fallthrough}.is_empty()`, `if !${s.fallthrough}.is_empty()`);
|
|
312
175
|
else
|
|
313
176
|
e.close(" else {");
|
|
314
|
-
// `_attrs` or `$attrs` alone is written as it is, with no `mergeProps` to normalise it.
|
|
315
177
|
if (parts.length === 1 && parts[0] === n) {
|
|
316
178
|
e.stmt(`fv::passed_attrs_into(out, ${s.fallthrough}.list(), ${isAttrs(n) && s.attrs !== null ? s.attrs : '""'});`);
|
|
317
179
|
e.close();
|
|
@@ -331,13 +193,12 @@ export function renderAttrs(s, e, n) {
|
|
|
331
193
|
else if (p.type === "ObjectExpression")
|
|
332
194
|
sources.push(`&[${attrEntries(s, p, "own").join(", ")}]`);
|
|
333
195
|
else
|
|
334
|
-
fail(s.comp, "attributes must be an object literal", p);
|
|
196
|
+
fail(s.comp, "FV0407", "attributes must be an object literal", p);
|
|
335
197
|
}
|
|
336
198
|
const ids = idsAt >= 0 && s.attrs !== null ? s.attrs : '""';
|
|
337
199
|
e.stmt(`fv::attrs_into(out, &[${sources.join(", ")}], ${idsAt >= 0 ? idsAt : sources.length}, ${ids});`);
|
|
338
200
|
e.close();
|
|
339
201
|
}
|
|
340
|
-
/** `ssrRenderAttrs(obj)` with nothing passed at run time: `$attrs` empty, `_attrs` the scope ids. */
|
|
341
202
|
function staticAttrs(s, e, n) {
|
|
342
203
|
if (isAttrs(n)) {
|
|
343
204
|
inheritedAttrs(s, e);
|
|
@@ -352,8 +213,6 @@ function staticAttrs(s, e, n) {
|
|
|
352
213
|
objectAttrs(s, e, merged, true);
|
|
353
214
|
return;
|
|
354
215
|
}
|
|
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
216
|
const before = new Set(mergeProps(s, { ...n, arguments: n.arguments.slice(0, at) }).properties.map((p) => p.key.name ?? p.key.value));
|
|
358
217
|
const part = (early) => ({ ...merged, properties: merged.properties.filter((p) => before.has(p.key.name ?? p.key.value) === early) });
|
|
359
218
|
objectAttrs(s, e, part(true), true);
|
|
@@ -363,22 +222,18 @@ function staticAttrs(s, e, n) {
|
|
|
363
222
|
}
|
|
364
223
|
objectAttrs(s, e, n, false);
|
|
365
224
|
}
|
|
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
225
|
function objectAttrs(s, e, n, merged) {
|
|
370
226
|
if (n.type !== "ObjectExpression")
|
|
371
|
-
fail(s.comp, "attributes must be an object literal", n);
|
|
227
|
+
fail(s.comp, "FV0407", "attributes must be an object literal", n);
|
|
372
228
|
for (const p of n.properties) {
|
|
373
229
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
374
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
230
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
375
231
|
const raw = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
376
232
|
if (IGNORED_PROPS.has(raw) || /^on[^a-z]/.test(raw) || raw.startsWith("."))
|
|
377
233
|
continue;
|
|
378
|
-
// Vue strips the `^` attribute-binding prefix before it looks at the name.
|
|
379
234
|
const key = raw.startsWith("^") ? raw.slice(1) : raw;
|
|
380
235
|
if (key === "className") {
|
|
381
|
-
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", p);
|
|
236
|
+
fail(s.comp, "FV0409", "`className` renders by its own rule in Vue; bind `class`", p);
|
|
382
237
|
}
|
|
383
238
|
else if (key === "class") {
|
|
384
239
|
const present = merged ? classPresent(s, p.value) : "true";
|
|
@@ -405,79 +260,6 @@ function objectAttrs(s, e, n, merged) {
|
|
|
405
260
|
}
|
|
406
261
|
}
|
|
407
262
|
}
|
|
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
263
|
export function mergeProps(s, n) {
|
|
482
264
|
const merged = new Map();
|
|
483
265
|
for (const a of n.arguments) {
|
|
@@ -486,17 +268,16 @@ export function mergeProps(s, n) {
|
|
|
486
268
|
if (directiveProps(s, a))
|
|
487
269
|
continue;
|
|
488
270
|
if (a.type !== "ObjectExpression")
|
|
489
|
-
fail(s.comp, "`_mergeProps` of object literals is supported", a);
|
|
271
|
+
fail(s.comp, "FV0410", "`_mergeProps` of object literals is supported", a);
|
|
490
272
|
for (const p of a.properties) {
|
|
491
273
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
492
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
274
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
493
275
|
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
494
276
|
const had = merged.get(key);
|
|
495
277
|
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
278
|
const before = literalClass(had.value);
|
|
498
279
|
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);
|
|
280
|
+
fail(s.comp, "FV0411", "a class merged with a string that may equal the class before it, which Vue then writes once: bind an array", p);
|
|
500
281
|
}
|
|
501
282
|
if (before !== p.value.value)
|
|
502
283
|
merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
|
|
@@ -505,7 +286,6 @@ export function mergeProps(s, n) {
|
|
|
505
286
|
merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
|
|
506
287
|
}
|
|
507
288
|
else if (had) {
|
|
508
|
-
// `Map` keeps a key where it was first set, as a JavaScript object does.
|
|
509
289
|
merged.set(key, { ...had, value: p.value });
|
|
510
290
|
}
|
|
511
291
|
else
|
|
@@ -514,14 +294,11 @@ export function mergeProps(s, n) {
|
|
|
514
294
|
}
|
|
515
295
|
return { type: "ObjectExpression", properties: [...merged.values()] };
|
|
516
296
|
}
|
|
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
297
|
export function attrEntries(s, obj, side) {
|
|
521
298
|
const out = [];
|
|
522
299
|
for (const p of obj.properties) {
|
|
523
300
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
524
|
-
fail(s.comp, "attribute objects hold plain keys", p);
|
|
301
|
+
fail(s.comp, "FV0408", "attribute objects hold plain keys", p);
|
|
525
302
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
526
303
|
if (IGNORED_PROPS.has(key) || /^on[^a-z]/.test(key) || key.startsWith("."))
|
|
527
304
|
continue;
|
|
@@ -529,10 +306,9 @@ export function attrEntries(s, obj, side) {
|
|
|
529
306
|
}
|
|
530
307
|
return out;
|
|
531
308
|
}
|
|
532
|
-
/** One attribute's value as an `fv::Attr`. */
|
|
533
309
|
export function attrOf(s, key, n, side) {
|
|
534
310
|
if (key === "className")
|
|
535
|
-
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", n);
|
|
311
|
+
fail(s.comp, "FV0409", "`className` renders by its own rule in Vue; bind `class`", n);
|
|
536
312
|
if (key === "class")
|
|
537
313
|
return classAttr(s, n, side);
|
|
538
314
|
if (key === "style")
|
|
@@ -541,11 +317,10 @@ export function attrOf(s, key, n, side) {
|
|
|
541
317
|
return "fv::Attr::Undefined";
|
|
542
318
|
return valueAttr(s, expr(s, n), n);
|
|
543
319
|
}
|
|
544
|
-
|
|
545
|
-
function valueAttr(s, v, n) {
|
|
320
|
+
export function valueAttr(s, v, n) {
|
|
546
321
|
switch (v.ty.k) {
|
|
547
322
|
case "str":
|
|
548
|
-
return `fv::Attr::str(${strArg(v.code)})`;
|
|
323
|
+
return isTemporary(v) ? `fv::Attr::Str(${asCow(v)})` : `fv::Attr::str(${strArg(v.code)})`;
|
|
549
324
|
case "int":
|
|
550
325
|
return `fv::Attr::Int(${bare(v.code)})`;
|
|
551
326
|
case "float":
|
|
@@ -553,6 +328,7 @@ function valueAttr(s, v, n) {
|
|
|
553
328
|
case "bool":
|
|
554
329
|
return `fv::Attr::Bool(${bare(v.code)})`;
|
|
555
330
|
case "undef":
|
|
331
|
+
case "null":
|
|
556
332
|
return "fv::Attr::Undefined";
|
|
557
333
|
case "opt": {
|
|
558
334
|
const of = v.ty.of.k;
|
|
@@ -561,209 +337,5 @@ function valueAttr(s, v, n) {
|
|
|
561
337
|
return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
|
|
562
338
|
}
|
|
563
339
|
}
|
|
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
|
-
}
|
|
340
|
+
return fail(s.comp, "FV0412", "an attribute that may fall through is a string, a number or a boolean", n);
|
|
769
341
|
}
|