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.
Files changed (86) hide show
  1. package/README.md +103 -15
  2. package/dist/attrs.d.ts +8 -42
  3. package/dist/attrs.js +148 -297
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +219 -0
  6. package/dist/children.d.ts +9 -0
  7. package/dist/children.js +273 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +121 -27
  11. package/dist/client.d.ts +11 -2
  12. package/dist/client.js +45 -9
  13. package/dist/compiler.d.ts +4 -0
  14. package/dist/compiler.js +37 -90
  15. package/dist/component.js +19 -82
  16. package/dist/context.d.ts +6 -68
  17. package/dist/context.js +25 -38
  18. package/dist/diff.d.ts +1 -0
  19. package/dist/diff.js +68 -0
  20. package/dist/emitter.d.ts +0 -8
  21. package/dist/emitter.js +0 -12
  22. package/dist/expr.d.ts +0 -70
  23. package/dist/expr.js +30 -855
  24. package/dist/fallthrough.d.ts +9 -0
  25. package/dist/fallthrough.js +78 -0
  26. package/dist/fixture.d.ts +2 -1
  27. package/dist/fixture.js +37 -22
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +1 -0
  30. package/dist/islands.d.ts +6 -0
  31. package/dist/islands.js +5 -0
  32. package/dist/lists.d.ts +0 -11
  33. package/dist/lists.js +4 -39
  34. package/dist/loops.d.ts +3 -0
  35. package/dist/loops.js +164 -0
  36. package/dist/model.d.ts +22 -130
  37. package/dist/model.js +37 -9
  38. package/dist/narrowing.d.ts +19 -0
  39. package/dist/narrowing.js +126 -0
  40. package/dist/numbers.d.ts +8 -0
  41. package/dist/numbers.js +73 -0
  42. package/dist/parens.d.ts +0 -27
  43. package/dist/parens.js +0 -44
  44. package/dist/plugin.d.ts +84 -0
  45. package/dist/plugin.js +26 -0
  46. package/dist/plugins/i18n.d.ts +23 -0
  47. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  48. package/dist/plugins/index.d.ts +2 -0
  49. package/dist/plugins/index.js +8 -0
  50. package/dist/plugins/router-link.d.ts +8 -0
  51. package/dist/{router.js → plugins/router-link.js} +65 -57
  52. package/dist/plugins/router.d.ts +42 -0
  53. package/dist/plugins/router.js +256 -0
  54. package/dist/plugins/scoped.d.ts +12 -0
  55. package/dist/{scoped.js → plugins/scoped.js} +55 -20
  56. package/dist/plugins/shared-types.d.ts +2 -0
  57. package/dist/plugins/shared-types.js +7 -0
  58. package/dist/plugins/stores.d.ts +37 -0
  59. package/dist/plugins/stores.js +377 -0
  60. package/dist/plugins/teleport.d.ts +5 -0
  61. package/dist/plugins/teleport.js +55 -0
  62. package/dist/rust.d.ts +2 -5
  63. package/dist/rust.js +66 -161
  64. package/dist/script.d.ts +0 -6
  65. package/dist/script.js +22 -121
  66. package/dist/slots.d.ts +13 -0
  67. package/dist/slots.js +160 -0
  68. package/dist/strings.d.ts +13 -0
  69. package/dist/strings.js +208 -0
  70. package/dist/styles.d.ts +14 -0
  71. package/dist/styles.js +181 -0
  72. package/dist/template.d.ts +2 -40
  73. package/dist/template.js +40 -691
  74. package/dist/testing.d.ts +1 -0
  75. package/dist/testing.js +1 -8
  76. package/dist/types.d.ts +2 -0
  77. package/dist/typescript.d.ts +0 -13
  78. package/dist/typescript.js +8 -41
  79. package/dist/vite.d.ts +1 -0
  80. package/dist/vite.js +35 -14
  81. package/package.json +11 -2
  82. package/dist/i18n.d.ts +0 -27
  83. package/dist/router.d.ts +0 -17
  84. package/dist/scoped.d.ts +0 -10
  85. package/dist/stores.d.ts +0 -6
  86. package/dist/stores.js +0 -227
package/dist/attrs.js CHANGED
@@ -1,15 +1,17 @@
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";
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 { cond, expr, known, meet, truthy, unquote } from "./expr.js";
6
- import { atom, bare, condition, logical, not, receiver, strArg } from "./parens.js";
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
- /** What a number written at run time is expected to take: most a page shows are shorter, and the
9
- * reservation is an estimate. */
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
- /** A string, a number or a boolean written escaped: now when it is known, by JavaScript itself. */
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
- /** `ssrRenderAttr(key, value)`: absent for null/undefined, `key="value"` for anything else. */
58
- export function renderAttr(e, key, v) {
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
- /** `ssrRenderAttrs(obj)`, key by key in the object's order, and the inherited scope ids where the
267
- * root's `_attrs` is merged in. */
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 isAttrs = (a) => a.type === "Identifier" && a.name === "_attrs";
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
- renderAttrs(s, e, merged);
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
- renderAttrs(s, e, part(true));
217
+ objectAttrs(s, e, part(true), true);
288
218
  inheritedAttrs(s, e);
289
- renderAttrs(s, e, part(false));
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
- renderStyle(s, e, p.value);
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.type === "Identifier" && a.name === "_attrs")
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 && (key === "class" || key === "style")) {
338
- merged.set(key, { ...p, value: { type: "ArrayExpression", elements: [had.value, p.value] } });
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
- /** The objects a style binding merges, in order: an object literal, a static `style` the compiler
351
- * parsed, `v-show`'s `cond ? null : { display: "none" }`, and arrays of those. */
352
- export function styleItems(s, n, when) {
353
- // A condition known now: an object that never applies is left out, one that always does is not
354
- // conditional.
355
- if (when === "false")
356
- return [];
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
- const first = flat.indexOf(at[0]);
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
- for (const key of order) {
463
- // The last place that sets it wins: walked back to the first that always applies.
464
- const chain = [];
465
- for (const set of sets.get(key).toReversed()) {
466
- chain.push(set);
467
- if (set.cond === null)
468
- break;
469
- }
470
- if (chain.length === 1 && chain[0].cond === null) {
471
- write(chain[0].css, chain[0].value);
472
- continue;
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
  }
@@ -0,0 +1,4 @@
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
+ export declare function call(s: Scope, n: N): Val;
3
+ export declare function isObjectCall(n: N, method: string): boolean;
4
+ export declare function helperCall(s: Scope, name: string, args: N[], n: N): Val;