ferrovue 0.2.0 → 0.4.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 +103 -15
- package/dist/attrs.d.ts +8 -42
- package/dist/attrs.js +148 -297
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +121 -27
- package/dist/client.d.ts +11 -2
- package/dist/client.js +45 -9
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +37 -90
- package/dist/component.js +19 -82
- package/dist/context.d.ts +6 -68
- package/dist/context.js +25 -38
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +68 -0
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +30 -855
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.js +5 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +22 -130
- package/dist/model.js +37 -9
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -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/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +65 -57
- 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/{scoped.js → plugins/scoped.js} +55 -20
- 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 +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +2 -5
- package/dist/rust.js +66 -161
- package/dist/script.d.ts +0 -6
- package/dist/script.js +22 -121
- 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 +2 -40
- package/dist/template.js +40 -691
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +35 -14
- package/package.json +11 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -10
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -227
package/dist/attrs.js
CHANGED
|
@@ -1,15 +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, 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
|
-
|
|
9
|
-
|
|
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";
|
|
10
13
|
const NUMBER_BYTES = 6;
|
|
11
|
-
|
|
12
|
-
function display(e, v) {
|
|
14
|
+
export function display(e, v) {
|
|
13
15
|
if (v.ty.k === "str" && known(v) !== undefined)
|
|
14
16
|
e.lit(escapeHtml(unquote(v.code)));
|
|
15
17
|
else if ((v.ty.k === "int" || v.ty.k === "float") && v.num !== undefined)
|
|
@@ -20,7 +22,6 @@ function display(e, v) {
|
|
|
20
22
|
e.stmt(`fv::escape_into(out, ${strArg(v.code)});`);
|
|
21
23
|
else if (v.ty.k === "int" || v.ty.k === "float") {
|
|
22
24
|
e.stmt(`fv::${v.ty.k === "int" ? "push_int" : "push_number"}(out, ${bare(v.code)});`);
|
|
23
|
-
// Counted in the reservation, or a page of numbers outgrows it and is copied to a larger one.
|
|
24
25
|
e.expect(NUMBER_BYTES);
|
|
25
26
|
}
|
|
26
27
|
else if (v.ty.k === "bool")
|
|
@@ -29,41 +30,43 @@ function display(e, v) {
|
|
|
29
30
|
return false;
|
|
30
31
|
return true;
|
|
31
32
|
}
|
|
32
|
-
/** `toDisplayString`, escaped. */
|
|
33
33
|
export function interpolate(e, v) {
|
|
34
34
|
if (display(e, v))
|
|
35
35
|
return;
|
|
36
36
|
switch (v.ty.k) {
|
|
37
37
|
case "undef":
|
|
38
38
|
return;
|
|
39
|
-
// `toDisplayString` of a query value: a string, nothing for `null`, an array as JSON.
|
|
40
|
-
case "query":
|
|
41
|
-
e.stmt(`${atom(v.code)}.write_display(out);`);
|
|
42
|
-
return;
|
|
43
39
|
case "opt":
|
|
44
40
|
e.open(`if let Some(v) = ${v.code}`);
|
|
45
41
|
interpolate(e, { code: "v", ty: v.ty.of });
|
|
46
42
|
e.close();
|
|
47
43
|
return;
|
|
48
44
|
default:
|
|
45
|
+
if (ctx.plugins.some((p) => p.values?.interpolate?.(e, v)))
|
|
46
|
+
return;
|
|
49
47
|
throw new GenError("only strings, numbers and booleans can be interpolated");
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
|
-
/** The value half of `key="value"`, escaped. */
|
|
53
50
|
export function attrValue(e, v) {
|
|
54
51
|
if (!display(e, v))
|
|
55
52
|
throw new GenError("an attribute value must be a string, a number or a boolean");
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
function renderable(s, key, v, n) {
|
|
55
|
+
const ty = v.ty.k === "opt" ? v.ty.of : v.ty;
|
|
56
|
+
if (ty.k === "str" || ty.k === "int" || ty.k === "float" || ty.k === "bool" || ty.k === "undef")
|
|
57
|
+
return;
|
|
58
|
+
const own = claim((p) => p.values?.unbindable?.(ty));
|
|
59
|
+
const what = own?.what ?? describeTy(ty);
|
|
60
|
+
const fix = own?.fix ?? (ty.k === "list" ? 'join it into one string, as `.join(",")`' : "bind a string, a number or a boolean");
|
|
61
|
+
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);
|
|
62
|
+
}
|
|
63
|
+
export function renderAttr(s, e, key, v, n) {
|
|
64
|
+
renderable(s, key, v, n);
|
|
59
65
|
if (v.ty.k === "undef")
|
|
60
66
|
return;
|
|
61
|
-
// Only a single query value is an attribute's value; `null` and an array leave it out.
|
|
62
|
-
if (v.ty.k === "query")
|
|
63
|
-
v = { code: `${atom(v.code)}.attr_value()`, ty: { k: "opt", of: STR } };
|
|
64
67
|
if (v.ty.k === "opt") {
|
|
65
68
|
e.open(`if let Some(v) = ${v.code}`);
|
|
66
|
-
renderAttr(e, key, { code: "v", ty: v.ty.of });
|
|
69
|
+
renderAttr(s, e, key, { code: "v", ty: v.ty.of }, n);
|
|
67
70
|
e.close();
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
@@ -71,19 +74,15 @@ export function renderAttr(e, key, v) {
|
|
|
71
74
|
attrValue(e, v);
|
|
72
75
|
e.lit(`"`);
|
|
73
76
|
}
|
|
74
|
-
/** `ssrRenderDynamicAttr(key, value)`: as `renderAttr`, but a boolean attribute is present or
|
|
75
|
-
* absent, and an empty string renders the name alone. */
|
|
76
77
|
export function renderDynamicAttr(s, e, key, v, n) {
|
|
77
78
|
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
78
79
|
if (!isSSRSafeAttrName(name))
|
|
79
80
|
fail(s.comp, `unsafe attribute name \`${name}\``, n);
|
|
81
|
+
renderable(s, key, v, n);
|
|
80
82
|
if (v.ty.k === "undef")
|
|
81
83
|
return;
|
|
82
|
-
if (v.ty.k === "query")
|
|
83
|
-
v = { code: `${atom(v.code)}.attr_value()`, ty: { k: "opt", of: STR } };
|
|
84
84
|
const boolean = (t) => isBooleanAttr(name) || (name === "hidden" && (t.k === "bool" || t.k === "int" || t.k === "float"));
|
|
85
85
|
if (v.ty.k === "opt" && boolean(v.ty.of)) {
|
|
86
|
-
// Present, or truthy when it is not a string: one test, with nothing to bind.
|
|
87
86
|
e.open(`if ${v.ty.of.k === "str" ? `${atom(v.code)}.is_some()` : truthy(v)}`);
|
|
88
87
|
e.lit(` ${name}`);
|
|
89
88
|
e.close();
|
|
@@ -115,7 +114,6 @@ export function renderDynamicAttr(s, e, key, v, n) {
|
|
|
115
114
|
e.lit(` ${name}`);
|
|
116
115
|
e.close(" else {");
|
|
117
116
|
e.lit(` ${name}="`);
|
|
118
|
-
// Re-evaluated rather than bound: every string expression here is a borrow or a pure call.
|
|
119
117
|
attrValue(e, v);
|
|
120
118
|
e.lit(`"`);
|
|
121
119
|
e.close();
|
|
@@ -125,132 +123,15 @@ export function renderDynamicAttr(s, e, key, v, n) {
|
|
|
125
123
|
attrValue(e, v);
|
|
126
124
|
e.lit(`"`);
|
|
127
125
|
}
|
|
128
|
-
/** `normalizeClass`'s view of a class binding, flattened: a string, an array of items, an object
|
|
129
|
-
* of `name: condition`, `cond && "name"`, `cond ? "a" : null`. Every item is trimmed and the empty
|
|
130
|
-
* ones dropped before they are joined with one space, which is what `class_into` does too, so an
|
|
131
|
-
* object's keys become items of their own. */
|
|
132
|
-
export function classItems(s, n) {
|
|
133
|
-
switch (n.type) {
|
|
134
|
-
case "StringLiteral": {
|
|
135
|
-
const t = n.value.trim();
|
|
136
|
-
return t ? [{ lit: t }] : [];
|
|
137
|
-
}
|
|
138
|
-
case "ArrayExpression":
|
|
139
|
-
return n.elements.flatMap((el) => (el ? classItems(s, el) : []));
|
|
140
|
-
case "ObjectExpression":
|
|
141
|
-
// A computed name may hold spaces, which Vue keeps between the names it joins: such an object
|
|
142
|
-
// is normalised whole, at run time, as Vue normalises it.
|
|
143
|
-
// Normalised whole, at run time, as a JavaScript object: when a name is computed (and may hold
|
|
144
|
-
// spaces, repeat another, or be an array index), or a literal name repeats or is an index —
|
|
145
|
-
// which a JavaScript object lists first, in numeric order.
|
|
146
|
-
const literalNames = n.properties
|
|
147
|
-
.filter((p) => p.type === "ObjectProperty" && !p.computed)
|
|
148
|
-
.map((p) => (p.key.type === "Identifier" ? p.key.name : String(p.key.value)));
|
|
149
|
-
const arrayIndex = (k) => /^(0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1;
|
|
150
|
-
if (n.properties.some((p) => p.type === "ObjectProperty" && p.computed) ||
|
|
151
|
-
literalNames.some(arrayIndex) ||
|
|
152
|
-
new Set(literalNames).size !== literalNames.length) {
|
|
153
|
-
const keys = [];
|
|
154
|
-
const entries = n.properties.map((p) => {
|
|
155
|
-
if (p.type !== "ObjectProperty")
|
|
156
|
-
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
157
|
-
const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
|
|
158
|
-
if (key.ty.k !== "str")
|
|
159
|
-
fail(s.comp, "a computed class name is a string", p.key);
|
|
160
|
-
// Names are told apart, which two halves of surrogate pairs would not be.
|
|
161
|
-
for (const other of keys)
|
|
162
|
-
meet(s.comp, other, key, "a class object's names", p.key, "equal");
|
|
163
|
-
keys.push(key);
|
|
164
|
-
return `(${bare(cond(s, p.value))}, ${bare(key.code)})`;
|
|
165
|
-
});
|
|
166
|
-
return [{ code: `&*fv::class_object(&[${entries.join(", ")}])` }];
|
|
167
|
-
}
|
|
168
|
-
return n.properties.flatMap((p) => {
|
|
169
|
-
if (p.type !== "ObjectProperty")
|
|
170
|
-
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
171
|
-
// A literal name is written as it is: an object's names are joined, not trimmed one by one.
|
|
172
|
-
let name;
|
|
173
|
-
if (!p.computed) {
|
|
174
|
-
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
175
|
-
if (key !== key.trim() || !key)
|
|
176
|
-
fail(s.comp, `class name \`${key}\` has spaces around it`, p);
|
|
177
|
-
name = { lit: key };
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
const k = expr(s, p.key);
|
|
181
|
-
if (k.ty.k !== "str")
|
|
182
|
-
fail(s.comp, "a computed class name is a string", p.key);
|
|
183
|
-
name = { code: `fv::js_trim(${strArg(k.code)})` };
|
|
184
|
-
}
|
|
185
|
-
const v = expr(s, p.value);
|
|
186
|
-
const on = known(v);
|
|
187
|
-
if (on !== undefined)
|
|
188
|
-
return on ? [name] : [];
|
|
189
|
-
const text = "lit" in name ? rustStr(name.lit) : name.code;
|
|
190
|
-
return [{ code: `if ${condition(truthy(v))} { ${bare(text)} } else { "" }` }];
|
|
191
|
-
});
|
|
192
|
-
case "LogicalExpression":
|
|
193
|
-
if (n.operator === "&&") {
|
|
194
|
-
return conditional(cond(s, n.left), classItems(s, n.right));
|
|
195
|
-
}
|
|
196
|
-
break;
|
|
197
|
-
case "ConditionalExpression":
|
|
198
|
-
if (n.consequent.type === "NullLiteral" || n.alternate.type === "NullLiteral") {
|
|
199
|
-
const branch = n.consequent.type === "NullLiteral" ? n.alternate : n.consequent;
|
|
200
|
-
const test = cond(s, n.test);
|
|
201
|
-
return conditional(n.consequent.type === "NullLiteral" ? not(test) : test, classItems(s, branch));
|
|
202
|
-
}
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
const v = expr(s, n);
|
|
206
|
-
if (v.ty.k === "str")
|
|
207
|
-
return [{ code: v.code }];
|
|
208
|
-
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
209
|
-
return [{ code: `${atom(v.code)}.unwrap_or("")` }];
|
|
210
|
-
if (v.ty.k === "undef")
|
|
211
|
-
return [];
|
|
212
|
-
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
213
|
-
}
|
|
214
|
-
/** Class items that apply only when \`test\` holds: each one, or nothing, decided now if it can be. */
|
|
215
|
-
function conditional(test, items) {
|
|
216
|
-
if (test === "true" || test === "false")
|
|
217
|
-
return test === "true" ? items : [];
|
|
218
|
-
return items.map((it) => ({ code: `if ${condition(test)} { ${"lit" in it ? rustStr(it.lit) : bare(it.code)} } else { "" }` }));
|
|
219
|
-
}
|
|
220
|
-
/** `ssrRenderClass(value)`: normalised, then escaped. `after` says a class was already written. */
|
|
221
|
-
export function renderClass(s, e, n, after = false) {
|
|
222
|
-
const items = classItems(s, n);
|
|
223
|
-
// Literal items before the first run-time one are written now; the rest go to `class_into`,
|
|
224
|
-
// which decides at run time whether each needs a separator.
|
|
225
|
-
let wrote = after;
|
|
226
|
-
let i = 0;
|
|
227
|
-
for (; i < items.length; i++) {
|
|
228
|
-
const it = items[i];
|
|
229
|
-
if (!("lit" in it))
|
|
230
|
-
break;
|
|
231
|
-
e.lit((wrote ? " " : "") + escapeHtml(it.lit));
|
|
232
|
-
wrote = true;
|
|
233
|
-
}
|
|
234
|
-
if (i < items.length) {
|
|
235
|
-
const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
|
|
236
|
-
e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
126
|
export const IGNORED_PROPS = new Set(["", "key", "ref", "innerHTML", "textContent", "ref_key", "ref_for"]);
|
|
240
|
-
/** `ssrRenderAttrs(_attrs)` on the root: the scope ids a parent handed it, already written as Vue
|
|
241
|
-
* writes them. They are all `_attrs` holds, since a parent passes a child only the props it
|
|
242
|
-
* declares and nothing else falls through. */
|
|
243
127
|
function inheritedAttrs(s, e) {
|
|
244
128
|
if (s.attrs !== null)
|
|
245
129
|
e.stmt(`out.push_str(${s.attrs});`);
|
|
246
130
|
}
|
|
247
|
-
/** `ssrGetDirectiveProps(_ctx, dir)`: a custom directive's server props, which are none for a
|
|
248
|
-
* directive the configuration declares client-only. `false` when `n` is not such a call. */
|
|
249
131
|
export function directiveProps(s, n) {
|
|
250
132
|
if (n?.type !== "CallExpression" || n.callee.type !== "Identifier" || n.callee.name !== "_ssrGetDirectiveProps")
|
|
251
133
|
return false;
|
|
252
134
|
const dir = n.arguments[1];
|
|
253
|
-
// `$setup["vFocus"]` for an imported directive, `_directive_focus` for a registered one.
|
|
254
135
|
let name = null;
|
|
255
136
|
if (dir?.type === "MemberExpression") {
|
|
256
137
|
const local = dir.computed ? dir.property.value : dir.property.name;
|
|
@@ -263,32 +144,84 @@ export function directiveProps(s, n) {
|
|
|
263
144
|
}
|
|
264
145
|
return true;
|
|
265
146
|
}
|
|
266
|
-
|
|
267
|
-
|
|
147
|
+
export function isAttrs(n) {
|
|
148
|
+
return n?.type === "Identifier" && n.name === "_attrs";
|
|
149
|
+
}
|
|
150
|
+
export function dollarAttrs(s, n) {
|
|
151
|
+
if (!isDollarAttrs(n, s.attrsBindings))
|
|
152
|
+
return false;
|
|
153
|
+
if (s.comp.idsInAttrs) {
|
|
154
|
+
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
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
export function mergedParts(n) {
|
|
159
|
+
return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_mergeProps" ? n.arguments : [n];
|
|
160
|
+
}
|
|
268
161
|
export function renderAttrs(s, e, n) {
|
|
269
|
-
const
|
|
162
|
+
const parts = mergedParts(n);
|
|
163
|
+
const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
164
|
+
if (!parts.some(passed)) {
|
|
165
|
+
staticAttrs(s, e, n);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
e.open(`if ${s.fallthrough}.is_empty()`);
|
|
169
|
+
const at = e.lines.length;
|
|
170
|
+
staticAttrs(s, e, n);
|
|
171
|
+
e.flush();
|
|
172
|
+
if (e.lines.length === at)
|
|
173
|
+
e.replace(at - 1, `if ${s.fallthrough}.is_empty()`, `if !${s.fallthrough}.is_empty()`);
|
|
174
|
+
else
|
|
175
|
+
e.close(" else {");
|
|
176
|
+
if (parts.length === 1 && parts[0] === n) {
|
|
177
|
+
e.stmt(`fv::passed_attrs_into(out, ${s.fallthrough}.list(), ${isAttrs(n) && s.attrs !== null ? s.attrs : '""'});`);
|
|
178
|
+
e.close();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const sources = [];
|
|
182
|
+
let idsAt = -1;
|
|
183
|
+
for (const p of parts) {
|
|
184
|
+
if (directiveProps(s, p))
|
|
185
|
+
continue;
|
|
186
|
+
if (isAttrs(p))
|
|
187
|
+
idsAt = sources.length;
|
|
188
|
+
if (passed(p))
|
|
189
|
+
sources.push(`${s.fallthrough}.list()`);
|
|
190
|
+
else if (isAttrs(p))
|
|
191
|
+
sources.push("&[]");
|
|
192
|
+
else if (p.type === "ObjectExpression")
|
|
193
|
+
sources.push(`&[${attrEntries(s, p, "own").join(", ")}]`);
|
|
194
|
+
else
|
|
195
|
+
fail(s.comp, "attributes must be an object literal", p);
|
|
196
|
+
}
|
|
197
|
+
const ids = idsAt >= 0 && s.attrs !== null ? s.attrs : '""';
|
|
198
|
+
e.stmt(`fv::attrs_into(out, &[${sources.join(", ")}], ${idsAt >= 0 ? idsAt : sources.length}, ${ids});`);
|
|
199
|
+
e.close();
|
|
200
|
+
}
|
|
201
|
+
function staticAttrs(s, e, n) {
|
|
270
202
|
if (isAttrs(n)) {
|
|
271
203
|
inheritedAttrs(s, e);
|
|
272
204
|
return;
|
|
273
205
|
}
|
|
274
|
-
if (directiveProps(s, n))
|
|
206
|
+
if (dollarAttrs(s, n) || directiveProps(s, n))
|
|
275
207
|
return;
|
|
276
208
|
if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_mergeProps") {
|
|
277
209
|
const merged = mergeProps(s, n);
|
|
278
210
|
const at = n.arguments.findIndex(isAttrs);
|
|
279
211
|
if (at < 0) {
|
|
280
|
-
|
|
212
|
+
objectAttrs(s, e, merged, true);
|
|
281
213
|
return;
|
|
282
214
|
}
|
|
283
|
-
// The inherited ids take their place among the keys: after those given before `_attrs`, before
|
|
284
|
-
// those first given after it, such as `v-show`'s `style` when there is no other.
|
|
285
215
|
const before = new Set(mergeProps(s, { ...n, arguments: n.arguments.slice(0, at) }).properties.map((p) => p.key.name ?? p.key.value));
|
|
286
216
|
const part = (early) => ({ ...merged, properties: merged.properties.filter((p) => before.has(p.key.name ?? p.key.value) === early) });
|
|
287
|
-
|
|
217
|
+
objectAttrs(s, e, part(true), true);
|
|
288
218
|
inheritedAttrs(s, e);
|
|
289
|
-
|
|
219
|
+
objectAttrs(s, e, part(false), true);
|
|
290
220
|
return;
|
|
291
221
|
}
|
|
222
|
+
objectAttrs(s, e, n, false);
|
|
223
|
+
}
|
|
224
|
+
function objectAttrs(s, e, n, merged) {
|
|
292
225
|
if (n.type !== "ObjectExpression")
|
|
293
226
|
fail(s.comp, "attributes must be an object literal", n);
|
|
294
227
|
for (const p of n.properties) {
|
|
@@ -297,19 +230,28 @@ export function renderAttrs(s, e, n) {
|
|
|
297
230
|
const raw = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
298
231
|
if (IGNORED_PROPS.has(raw) || /^on[^a-z]/.test(raw) || raw.startsWith("."))
|
|
299
232
|
continue;
|
|
300
|
-
// Vue strips the `^` attribute-binding prefix before it looks at the name.
|
|
301
233
|
const key = raw.startsWith("^") ? raw.slice(1) : raw;
|
|
302
234
|
if (key === "className") {
|
|
303
235
|
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", p);
|
|
304
236
|
}
|
|
305
237
|
else if (key === "class") {
|
|
238
|
+
const present = merged ? classPresent(s, p.value) : "true";
|
|
239
|
+
if (present === "false")
|
|
240
|
+
continue;
|
|
241
|
+
if (present !== "true")
|
|
242
|
+
e.open(`if ${condition(present)}`);
|
|
306
243
|
e.lit(` class="`);
|
|
307
244
|
renderClass(s, e, p.value);
|
|
308
245
|
e.lit(`"`);
|
|
246
|
+
if (present !== "true")
|
|
247
|
+
e.close();
|
|
309
248
|
}
|
|
310
249
|
else if (key === "style") {
|
|
311
250
|
e.lit(` style="`);
|
|
312
|
-
|
|
251
|
+
if (merged)
|
|
252
|
+
mergedStyle(s, e, p.value);
|
|
253
|
+
else
|
|
254
|
+
renderStyle(s, e, p.value);
|
|
313
255
|
e.lit(`"`);
|
|
314
256
|
}
|
|
315
257
|
else {
|
|
@@ -317,13 +259,10 @@ export function renderAttrs(s, e, n) {
|
|
|
317
259
|
}
|
|
318
260
|
}
|
|
319
261
|
}
|
|
320
|
-
/** `mergeProps(a, _attrs, b, …)` as one object literal, without `_attrs`: it holds only scope ids,
|
|
321
|
-
* which the caller writes after the rest. Of the rest, `class` and `style` values are merged as an
|
|
322
|
-
* array, and any other key keeps the place it first had with the last value given. */
|
|
323
262
|
export function mergeProps(s, n) {
|
|
324
263
|
const merged = new Map();
|
|
325
264
|
for (const a of n.arguments) {
|
|
326
|
-
if (a
|
|
265
|
+
if (isAttrs(a) || dollarAttrs(s, a))
|
|
327
266
|
continue;
|
|
328
267
|
if (directiveProps(s, a))
|
|
329
268
|
continue;
|
|
@@ -334,11 +273,18 @@ export function mergeProps(s, n) {
|
|
|
334
273
|
fail(s.comp, "attribute objects hold plain keys", p);
|
|
335
274
|
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
336
275
|
const had = merged.get(key);
|
|
337
|
-
if (had &&
|
|
338
|
-
|
|
276
|
+
if (had && key === "class" && maybeEqual(s, p.value)) {
|
|
277
|
+
const before = literalClass(had.value);
|
|
278
|
+
if (before === null || p.value.type !== "StringLiteral") {
|
|
279
|
+
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
|
+
}
|
|
281
|
+
if (before !== p.value.value)
|
|
282
|
+
merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
|
|
283
|
+
}
|
|
284
|
+
else if (had && (key === "class" || key === "style")) {
|
|
285
|
+
merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value], fvMerged: true } });
|
|
339
286
|
}
|
|
340
287
|
else if (had) {
|
|
341
|
-
// `Map` keeps a key where it was first set, as a JavaScript object does.
|
|
342
288
|
merged.set(key, { ...had, value: p.value });
|
|
343
289
|
}
|
|
344
290
|
else
|
|
@@ -347,142 +293,47 @@ export function mergeProps(s, n) {
|
|
|
347
293
|
}
|
|
348
294
|
return { type: "ObjectExpression", properties: [...merged.values()] };
|
|
349
295
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (when === "true")
|
|
358
|
-
when = null;
|
|
359
|
-
const both = (a, b) => (a === null ? b : logical(a, "&&", b));
|
|
360
|
-
switch (n.type) {
|
|
361
|
-
case "NullLiteral":
|
|
362
|
-
return [];
|
|
363
|
-
case "ArrayExpression":
|
|
364
|
-
return n.elements.flatMap((el) => (el ? styleItems(s, el, when) : []));
|
|
365
|
-
case "StringLiteral":
|
|
366
|
-
// Parsed as `normalizeStyle` parses a string inside an array, by Vue's own function.
|
|
367
|
-
return [{ cond: when, entries: Object.entries(parseStringStyle(n.value)).map(([key, v]) => ({ key, css: key, value: { type: "StringLiteral", value: v } })) }];
|
|
368
|
-
case "ConditionalExpression": {
|
|
369
|
-
const t = cond(s, n.test);
|
|
370
|
-
return [...styleItems(s, n.consequent, both(when, t)), ...styleItems(s, n.alternate, both(when, not(t)))];
|
|
371
|
-
}
|
|
372
|
-
case "LogicalExpression":
|
|
373
|
-
if (n.operator === "&&")
|
|
374
|
-
return styleItems(s, n.right, both(when, cond(s, n.left)));
|
|
375
|
-
break;
|
|
376
|
-
case "ObjectExpression":
|
|
377
|
-
return [{
|
|
378
|
-
cond: when,
|
|
379
|
-
entries: n.properties.map((p) => {
|
|
380
|
-
if (p.type !== "ObjectProperty" || p.computed)
|
|
381
|
-
fail(s.comp, "a style object holds plain `property: value` pairs", p);
|
|
382
|
-
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
383
|
-
// An integer-like key comes first in a JavaScript object whatever the order written.
|
|
384
|
-
if (/^\d+$/.test(key) || key.startsWith(":"))
|
|
385
|
-
fail(s.comp, `style property \`${key}\``, p);
|
|
386
|
-
return { key, css: key.startsWith("--") ? key : hyphenate(key), value: p.value };
|
|
387
|
-
}),
|
|
388
|
-
}];
|
|
389
|
-
}
|
|
390
|
-
return fail(s.comp, "a style binding is an object, an array of objects, or a string on its own", n);
|
|
391
|
-
}
|
|
392
|
-
/** `ssrRenderStyle(value)`: a string as it is, or the objects merged — each property where it first
|
|
393
|
-
* appears, with the last value given — and written `name:value;`, all escaped. */
|
|
394
|
-
export function renderStyle(s, e, n) {
|
|
395
|
-
// A string on its own is written as it is, with no normalising.
|
|
396
|
-
if (n.type === "StringLiteral") {
|
|
397
|
-
e.lit(escapeHtml(n.value));
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (n.type !== "ObjectExpression" && n.type !== "ArrayExpression" && n.type !== "ConditionalExpression" && n.type !== "LogicalExpression" && n.type !== "NullLiteral") {
|
|
401
|
-
const v = expr(s, n);
|
|
402
|
-
if (v.ty.k === "str")
|
|
403
|
-
e.stmt(`fv::escape_into(out, ${strArg(v.code)});`);
|
|
404
|
-
else if (v.ty.k === "opt" && v.ty.of.k === "str") {
|
|
405
|
-
e.open(`if let Some(v) = ${v.code}`);
|
|
406
|
-
e.stmt("fv::escape_into(out, v);");
|
|
407
|
-
e.close();
|
|
408
|
-
}
|
|
409
|
-
else if (v.ty.k !== "undef")
|
|
410
|
-
fail(s.comp, "a style binding is a string, an object or an array", n);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
const items = styleItems(s, n, null);
|
|
414
|
-
// Each property, where it first appears, with every place that sets it.
|
|
415
|
-
const order = [];
|
|
416
|
-
const sets = new Map();
|
|
417
|
-
for (const it of items) {
|
|
418
|
-
for (const en of it.entries) {
|
|
419
|
-
if (!sets.has(en.key)) {
|
|
420
|
-
order.push(en.key);
|
|
421
|
-
sets.set(en.key, []);
|
|
422
|
-
}
|
|
423
|
-
sets.get(en.key).push({ cond: it.cond, css: en.css, value: en.value });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
const write = (css, value) => {
|
|
427
|
-
// A literal is stringified now, by JavaScript itself: `0.5`, `"1px"`.
|
|
428
|
-
if (value.type === "StringLiteral" || value.type === "NumericLiteral") {
|
|
429
|
-
e.lit(escapeHtml(`${css}:${String(value.value)};`));
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const v = expr(s, value);
|
|
433
|
-
const one = (w) => {
|
|
434
|
-
if (w.ty.k === "str" || w.ty.k === "int" || w.ty.k === "float") {
|
|
435
|
-
e.lit(`${escapeHtml(css)}:`);
|
|
436
|
-
display(e, w);
|
|
437
|
-
e.lit(";");
|
|
438
|
-
}
|
|
439
|
-
else if (w.ty.k === "opt") {
|
|
440
|
-
e.open(`if let Some(v) = ${w.code}`);
|
|
441
|
-
one({ code: "v", ty: w.ty.of });
|
|
442
|
-
e.close();
|
|
443
|
-
}
|
|
444
|
-
// A boolean or `undefined` writes nothing — and, set later, removes what was set before.
|
|
445
|
-
};
|
|
446
|
-
one(v);
|
|
447
|
-
};
|
|
448
|
-
// A property's place is where an object that is present first sets it. When that object is
|
|
449
|
-
// conditional and the property is set again after another one first appears, the order would be
|
|
450
|
-
// decided at run time.
|
|
451
|
-
const flat = items.flatMap((it, i) => it.entries.map((en) => ({ key: en.key, item: i, cond: it.cond })));
|
|
452
|
-
for (const key of order) {
|
|
453
|
-
const at = flat.filter((f) => f.key === key);
|
|
454
|
-
if (at.length < 2 || at[0].cond === null)
|
|
296
|
+
export function attrEntries(s, obj, side) {
|
|
297
|
+
const out = [];
|
|
298
|
+
for (const p of obj.properties) {
|
|
299
|
+
if (p.type !== "ObjectProperty" || p.computed)
|
|
300
|
+
fail(s.comp, "attribute objects hold plain keys", p);
|
|
301
|
+
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
302
|
+
if (IGNORED_PROPS.has(key) || /^on[^a-z]/.test(key) || key.startsWith("."))
|
|
455
303
|
continue;
|
|
456
|
-
|
|
457
|
-
const again = flat.indexOf(at[at.length - 1]);
|
|
458
|
-
const between = flat.slice(first + 1, again).some((f) => order.indexOf(f.key) > order.indexOf(key));
|
|
459
|
-
if (between)
|
|
460
|
-
fail(s.comp, `the place of style property \`${key}\` would depend on a condition; set it unconditionally first`, n);
|
|
304
|
+
out.push(`(${rustStr(key)}, ${attrOf(s, key, p.value, side)})`);
|
|
461
305
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
export function attrOf(s, key, n, side) {
|
|
309
|
+
if (key === "className")
|
|
310
|
+
fail(s.comp, "`className` renders by its own rule in Vue; bind `class`", n);
|
|
311
|
+
if (key === "class")
|
|
312
|
+
return classAttr(s, n, side);
|
|
313
|
+
if (key === "style")
|
|
314
|
+
return styleAttr(s, n);
|
|
315
|
+
if (n.type === "NullLiteral")
|
|
316
|
+
return "fv::Attr::Undefined";
|
|
317
|
+
return valueAttr(s, expr(s, n), n);
|
|
318
|
+
}
|
|
319
|
+
export function valueAttr(s, v, n) {
|
|
320
|
+
switch (v.ty.k) {
|
|
321
|
+
case "str":
|
|
322
|
+
return isTemporary(v) ? `fv::Attr::Str(${asCow(v)})` : `fv::Attr::str(${strArg(v.code)})`;
|
|
323
|
+
case "int":
|
|
324
|
+
return `fv::Attr::Int(${bare(v.code)})`;
|
|
325
|
+
case "float":
|
|
326
|
+
return `fv::Attr::Float(${bare(v.code)})`;
|
|
327
|
+
case "bool":
|
|
328
|
+
return `fv::Attr::Bool(${bare(v.code)})`;
|
|
329
|
+
case "undef":
|
|
330
|
+
return "fv::Attr::Undefined";
|
|
331
|
+
case "opt": {
|
|
332
|
+
const of = v.ty.of.k;
|
|
333
|
+
const make = of === "str" ? "fv::Attr::str" : of === "int" ? "fv::Attr::Int" : of === "float" ? "fv::Attr::Float" : of === "bool" ? "fv::Attr::Bool" : null;
|
|
334
|
+
if (make !== null)
|
|
335
|
+
return `${atom(v.code)}.map_or(fv::Attr::Undefined, ${make})`;
|
|
473
336
|
}
|
|
474
|
-
chain.forEach((set, i) => {
|
|
475
|
-
if (set.cond === null) {
|
|
476
|
-
e.close(" else {");
|
|
477
|
-
}
|
|
478
|
-
else if (i === 0) {
|
|
479
|
-
e.open(`if ${condition(set.cond)}`);
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
e.close(` else if ${condition(set.cond)} {`);
|
|
483
|
-
}
|
|
484
|
-
write(set.css, set.value);
|
|
485
|
-
});
|
|
486
|
-
e.close();
|
|
487
337
|
}
|
|
338
|
+
return fail(s.comp, "an attribute that may fall through is a string, a number or a boolean", n);
|
|
488
339
|
}
|
package/dist/calls.d.ts
ADDED