ferrovue 0.3.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 +32 -13
- package/dist/attrs.d.ts +2 -52
- package/dist/attrs.js +18 -448
- 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 +0 -17
- package/dist/client.js +0 -12
- package/dist/compiler.js +25 -90
- package/dist/component.js +10 -86
- package/dist/context.d.ts +4 -66
- package/dist/context.js +2 -33
- 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/expr.d.ts +0 -70
- package/dist/expr.js +27 -890
- 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 +1 -0
- package/dist/islands.js +1 -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 +14 -143
- package/dist/model.js +27 -12
- 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} +19 -61
- 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 +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +38 -160
- package/dist/script.d.ts +0 -6
- package/dist/script.js +13 -128
- 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 +14 -738
- 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 +1 -23
- package/package.json +7 -2
- 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/script.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
/* `<script setup>` read: what the server evaluates, and what stays on the client. */
|
|
2
1
|
import { basename } from "node:path";
|
|
3
|
-
import { fail, GenError, snake,
|
|
2
|
+
import { fail, GenError, snake, takesAttrs } from "./model.js";
|
|
4
3
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
5
4
|
import { definePropsType } from "./typescript.js";
|
|
6
|
-
import { expr, fieldVal
|
|
5
|
+
import { expr, fieldVal } from "./expr.js";
|
|
7
6
|
import { collected, heldList } from "./lists.js";
|
|
8
|
-
import { storeImport } from "./stores.js";
|
|
9
7
|
import { bare, operand, UNARY } from "./parens.js";
|
|
10
|
-
|
|
8
|
+
import { known } from "./narrowing.js";
|
|
11
9
|
export const CLIENT_HOOKS = new Set([
|
|
12
10
|
"onMounted", "onBeforeMount", "onUnmounted", "onBeforeUnmount", "onUpdated", "onBeforeUpdate",
|
|
13
11
|
"onActivated", "onDeactivated", "onErrorCaptured", "onRenderTracked", "onRenderTriggered",
|
|
14
12
|
]);
|
|
15
|
-
/** Compiler macros and calls with no effect on the server's render. */
|
|
16
13
|
export const INERT_CALLS = new Set(["defineEmits", "defineSlots", "defineOptions", "defineExpose", "defineProps", "withDefaults", "provide"]);
|
|
17
|
-
/** A statement in setup that is a call on its own: allowed only when it cannot change the render. */
|
|
18
14
|
export function setupStatement(comp, st) {
|
|
19
15
|
const c = st.expression;
|
|
20
16
|
const name = c?.type === "CallExpression" && c.callee.type === "Identifier" ? c.callee.name : null;
|
|
21
17
|
if (name !== null && (CLIENT_HOOKS.has(name) || INERT_CALLS.has(name)))
|
|
22
18
|
return;
|
|
23
19
|
if (name === "watch") {
|
|
24
|
-
// A watcher's callback runs on the server only with `immediate`, and could then change state.
|
|
25
20
|
const options = c.arguments[2];
|
|
26
21
|
const immediate = options?.type === "ObjectExpression" && options.properties.some((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "immediate" && !(p.value.type === "BooleanLiteral" && !p.value.value));
|
|
27
22
|
if (!immediate)
|
|
@@ -35,8 +30,6 @@ export function setupStatement(comp, st) {
|
|
|
35
30
|
fail(comp, "`onServerPrefetch` fetches on the server; pass the data in as props instead", st);
|
|
36
31
|
fail(comp, `\`${st.expression?.type === "CallExpression" ? `${name ?? "a call"}()` : st.expression?.type}\` in setup could change what renders, and is not translated`, st);
|
|
37
32
|
}
|
|
38
|
-
/** What a setup binding's value is computed from: `ref(x)` and `shallowRef(x)` hold `x`,
|
|
39
|
-
* `computed(() => x)` is `x`, and a plain `const y = x` is `x`. */
|
|
40
33
|
export function setupSource(init) {
|
|
41
34
|
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
|
|
42
35
|
const name = init.callee.name;
|
|
@@ -56,7 +49,6 @@ export function setupSource(init) {
|
|
|
56
49
|
return setupSource(init.expression);
|
|
57
50
|
return init;
|
|
58
51
|
}
|
|
59
|
-
/** The names a destructuring pattern binds. */
|
|
60
52
|
export function patternNames(p) {
|
|
61
53
|
switch (p?.type) {
|
|
62
54
|
case "Identifier":
|
|
@@ -87,25 +79,20 @@ export function scopeFor(comp, ast, components) {
|
|
|
87
79
|
narrowed: new Map(),
|
|
88
80
|
selfAlias: { name: null },
|
|
89
81
|
helperBytes: { n: 0 },
|
|
90
|
-
router: new Map(),
|
|
91
82
|
directives: new Map(),
|
|
92
|
-
i18nT: new Set(),
|
|
93
83
|
fill: false,
|
|
94
84
|
vnode: false,
|
|
95
|
-
// A component a parent may pass attributes takes them with the scope ids, as one `fv::Attrs`.
|
|
96
85
|
attrs: comp.inherits ? (takesAttrs(comp) ? "fv_attrs.ids()" : "fv_attrs") : null,
|
|
97
86
|
fallthrough: takesAttrs(comp) ? "fv_attrs" : null,
|
|
98
87
|
attrsBindings: new Set(),
|
|
99
88
|
sid: null,
|
|
89
|
+
plugins: new Map(),
|
|
100
90
|
};
|
|
91
|
+
for (const p of ctx.plugins)
|
|
92
|
+
if (p.scope)
|
|
93
|
+
scope.plugins.set(p, p.scope(scope));
|
|
101
94
|
const lets = [];
|
|
102
|
-
const storeHooks = new Map();
|
|
103
|
-
let storeToRefsName = null;
|
|
104
|
-
let useRouteName = null;
|
|
105
|
-
let useI18nName = null;
|
|
106
95
|
let useAttrsName = null;
|
|
107
|
-
/** Setup bindings holding a store, by name. */
|
|
108
|
-
const storeValues = new Map();
|
|
109
96
|
for (const st of ast) {
|
|
110
97
|
if (st.type === "ImportDeclaration") {
|
|
111
98
|
const from = st.source.value;
|
|
@@ -113,54 +100,16 @@ export function scopeFor(comp, ast, components) {
|
|
|
113
100
|
const def = st.specifiers.find((x) => x.type === "ImportDefaultSpecifier");
|
|
114
101
|
if (def)
|
|
115
102
|
scope.children.set(def.local.name, basename(from, ".vue"));
|
|
103
|
+
continue;
|
|
116
104
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// A type from the store's file, which `readComponent` has already resolved.
|
|
121
|
-
if (st.importKind === "type" || sp.importKind === "type" || (hook !== null && ctx.storeStructs.has(hook)))
|
|
122
|
-
continue;
|
|
123
|
-
const store = hook ? ctx.stores.get(hook) : undefined;
|
|
124
|
-
if (!store || store.module !== storeImport(comp, from))
|
|
125
|
-
fail(comp, "import a store by its `use…` hook", sp);
|
|
126
|
-
storeHooks.set(sp.local.name, store);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
else if (from === "vue-router") {
|
|
130
|
-
for (const sp of st.specifiers) {
|
|
131
|
-
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
132
|
-
if (name === "useRoute")
|
|
133
|
-
useRouteName = sp.local.name;
|
|
134
|
-
else if (name === "RouterLink" || name === "RouterView")
|
|
135
|
-
scope.router.set(sp.local.name, name);
|
|
136
|
-
else
|
|
137
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-router does not run on the server`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else if (from === "vue-i18n") {
|
|
141
|
-
for (const sp of st.specifiers) {
|
|
142
|
-
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
143
|
-
if (name === "useI18n")
|
|
144
|
-
useI18nName = sp.local.name;
|
|
145
|
-
else
|
|
146
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-i18n does not run on the server`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
else if (from === "vue") {
|
|
105
|
+
if (ctx.plugins.some((p) => p.scriptImport?.(scope, st, from)))
|
|
106
|
+
continue;
|
|
107
|
+
if (from === "vue") {
|
|
150
108
|
for (const sp of st.specifiers) {
|
|
151
109
|
if (sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === "useAttrs")
|
|
152
110
|
useAttrsName = sp.local.name;
|
|
153
111
|
}
|
|
154
112
|
}
|
|
155
|
-
else if (from === "pinia") {
|
|
156
|
-
for (const sp of st.specifiers) {
|
|
157
|
-
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
158
|
-
if (name === "storeToRefs")
|
|
159
|
-
storeToRefsName = sp.local.name;
|
|
160
|
-
else
|
|
161
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from Pinia does not run on the server`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
113
|
else if (ctx.helperModule !== null && from === ctx.helperModule) {
|
|
165
114
|
for (const sp of st.specifiers) {
|
|
166
115
|
if (!ctx.helpers[sp.imported.name])
|
|
@@ -182,14 +131,10 @@ export function scopeFor(comp, ast, components) {
|
|
|
182
131
|
setupStatement(comp, st);
|
|
183
132
|
continue;
|
|
184
133
|
}
|
|
185
|
-
// Setup runs on the server too, so a statement there can change what renders: `x.value = ...`
|
|
186
|
-
// after a `ref`, an `if`, a loop. None of it is translated, so none of it may exist.
|
|
187
134
|
if (st.type !== "VariableDeclaration")
|
|
188
135
|
fail(comp, `\`${st.type}\` in setup is not supported`, st);
|
|
189
136
|
for (const d of st.declarations) {
|
|
190
137
|
const init0 = d.init;
|
|
191
|
-
// `const { size = "md", label: l } = defineProps<...>()`: each name reads that prop, whose
|
|
192
|
-
// default `compileScript` already resolved.
|
|
193
138
|
if (d.id.type === "ObjectPattern" && definePropsType(init0)) {
|
|
194
139
|
for (const p of d.id.properties) {
|
|
195
140
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
@@ -202,73 +147,19 @@ export function scopeFor(comp, ast, components) {
|
|
|
202
147
|
}
|
|
203
148
|
continue;
|
|
204
149
|
}
|
|
205
|
-
|
|
206
|
-
// `const { t, locale } = useI18n()`: \`t\` translates, \`locale\` is the request's locale.
|
|
207
|
-
if (d.id.type === "ObjectPattern" && calls(useI18nName)) {
|
|
208
|
-
for (const p of d.id.properties) {
|
|
209
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
210
|
-
fail(comp, "`useI18n()` is destructured into plain names", p);
|
|
211
|
-
}
|
|
212
|
-
const key = p.key.name ?? p.key.value;
|
|
213
|
-
if (key === "t")
|
|
214
|
-
scope.i18nT.add(p.value.name);
|
|
215
|
-
else if (key === "locale") {
|
|
216
|
-
scope.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
|
|
217
|
-
scope.refs.add(p.value.name);
|
|
218
|
-
}
|
|
219
|
-
else
|
|
220
|
-
scope.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
|
|
221
|
-
}
|
|
222
|
-
comp.readsI18n = true;
|
|
150
|
+
if (ctx.plugins.some((p) => p.scriptBinding?.(scope, d)))
|
|
223
151
|
continue;
|
|
224
|
-
}
|
|
225
|
-
// `const { density, label: l } = storeToRefs(prefs)`: each name is that field of the state.
|
|
226
|
-
if (d.id.type === "ObjectPattern" && calls(storeToRefsName)) {
|
|
227
|
-
const arg = init0.arguments[0];
|
|
228
|
-
const store = arg?.type === "Identifier" ? storeValues.get(arg.name) : undefined;
|
|
229
|
-
if (!store || init0.arguments.length !== 1)
|
|
230
|
-
fail(comp, "`storeToRefs` takes a store bound in this setup", d);
|
|
231
|
-
for (const p of d.id.properties) {
|
|
232
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
233
|
-
fail(comp, "`storeToRefs` is destructured into plain names", p);
|
|
234
|
-
}
|
|
235
|
-
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
236
|
-
const state = { code: `fv_stores.${store.field}`, ty: { k: "struct", name: store.state, store: true } };
|
|
237
|
-
scope.setup.set(p.value.name, storeGetter(scope, state, key, p) ?? fieldVal(comp, state.code, state.ty, key, p));
|
|
238
|
-
scope.refs.add(p.value.name);
|
|
239
|
-
}
|
|
240
|
-
comp.readsStores = true;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
152
|
if (d.id.type !== "Identifier") {
|
|
244
|
-
// Destructuring anything else: client-side state the template may not read.
|
|
245
153
|
for (const name of patternNames(d.id))
|
|
246
154
|
scope.clientOnly.set(name, "it is destructured from a value the server does not have");
|
|
247
155
|
continue;
|
|
248
156
|
}
|
|
249
157
|
const local = d.id.name;
|
|
250
|
-
// `const prefs = usePrefs()`: the store's state, as the server was given it.
|
|
251
|
-
const hook = init0?.type === "CallExpression" && init0.callee.type === "Identifier" ? storeHooks.get(init0.callee.name) : undefined;
|
|
252
|
-
if (hook) {
|
|
253
|
-
if (init0.arguments.length)
|
|
254
|
-
fail(comp, "a store hook takes no arguments", d);
|
|
255
|
-
storeValues.set(local, hook);
|
|
256
|
-
scope.setup.set(local, { code: `fv_stores.${hook.field}`, ty: { k: "struct", name: hook.state, store: true } });
|
|
257
|
-
comp.readsStores = true;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
// `const route = useRoute()`: the reader's location.
|
|
261
|
-
if (useRouteName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useRouteName) {
|
|
262
|
-
scope.setup.set(local, theRoute(scope, d));
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
// `const attrs = useAttrs()`: `$attrs`, which the template may bind whole.
|
|
266
158
|
if (useAttrsName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useAttrsName) {
|
|
267
159
|
scope.attrsBindings.add(local);
|
|
268
160
|
scope.clientOnly.set(local, "`useAttrs()` is bound whole, with `v-bind`; a value read from it has no type");
|
|
269
161
|
continue;
|
|
270
162
|
}
|
|
271
|
-
// `const model = defineModel<string>()`: the model's prop.
|
|
272
163
|
const model = comp.models.get(local);
|
|
273
164
|
if (model !== undefined) {
|
|
274
165
|
scope.setup.set(local, fieldVal(comp, "props", { k: "struct", name: "Props" }, model, d));
|
|
@@ -291,9 +182,6 @@ export function scopeFor(comp, ast, components) {
|
|
|
291
182
|
scope.clientOnly.set(local, "it is not a value the server computes: only `ref(…)`, `computed(() => …)` and plain expressions are");
|
|
292
183
|
continue;
|
|
293
184
|
}
|
|
294
|
-
// Evaluated once, as setup is on the server. A value the server cannot evaluate is client-side
|
|
295
|
-
// state — a template ref, an element, a `reactive` object — which the template may still name
|
|
296
|
-
// from an event handler; a reference the server needs fails in `expr` with this reason.
|
|
297
185
|
let v;
|
|
298
186
|
try {
|
|
299
187
|
v = expr(scope, source);
|
|
@@ -304,25 +192,22 @@ export function scopeFor(comp, ast, components) {
|
|
|
304
192
|
scope.clientOnly.set(local, e.message.replace(/^[^:]*: /, ""));
|
|
305
193
|
continue;
|
|
306
194
|
}
|
|
307
|
-
if (v.ty.k === "undef") {
|
|
195
|
+
if (v.ty.k === "undef" || (v.ty.k === "str" && known(v) !== undefined)) {
|
|
308
196
|
scope.setup.set(local, v);
|
|
309
197
|
continue;
|
|
310
198
|
}
|
|
311
199
|
const name = `s_${snake(local).replace(/^r#/, "")}`;
|
|
312
200
|
const lone = v.lone ? { lone: true } : {};
|
|
313
|
-
// A computed list is kept as its items, and read again by item.
|
|
314
201
|
if (v.ty.k === "list" && v.iter !== undefined) {
|
|
315
202
|
lets.push(`let ${name} = ${collected(v)};`);
|
|
316
203
|
scope.setup.set(local, heldList(name, v.ty.of, v.lone));
|
|
317
204
|
continue;
|
|
318
205
|
}
|
|
319
|
-
// An optional string a temporary owns is kept as that `Option<Cow>`.
|
|
320
206
|
if (v.held !== undefined) {
|
|
321
207
|
lets.push(`let ${name} = ${v.held};`);
|
|
322
208
|
scope.setup.set(local, { code: `${name}.as_deref()`, ty: v.ty, ...lone });
|
|
323
209
|
continue;
|
|
324
210
|
}
|
|
325
|
-
// A list, a record or an object is a place in the props: borrowed, never moved out of them.
|
|
326
211
|
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
327
212
|
lets.push(`let ${name} = ${place ? `&${operand(v.code, UNARY)}` : bare(v.code)};`);
|
|
328
213
|
scope.setup.set(local, { code: name, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) });
|
package/dist/slots.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
|
|
2
|
+
import { Emitter } from "./emitter.ts";
|
|
3
|
+
export declare function slotBody(s: Scope, value: N): N[];
|
|
4
|
+
export declare function slotContent(s: Scope, value: N): {
|
|
5
|
+
body: N[];
|
|
6
|
+
param: N;
|
|
7
|
+
};
|
|
8
|
+
export declare function staticallyFilled(body: N[]): boolean;
|
|
9
|
+
export declare function slotTypeName(slotName: string, suffix: string): string;
|
|
10
|
+
export declare function slotFieldBorrows(ty: Ty): boolean;
|
|
11
|
+
export declare function slotFieldTy(ty: Ty, comp: Component): string;
|
|
12
|
+
export declare function slotFieldValue(s: Scope, v: Val, n: N): string;
|
|
13
|
+
export declare function slotOutlet(s: Scope, e: Emitter, c: N): void;
|
package/dist/slots.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { fail, GenError, rustStr, snake } from "./model.js";
|
|
2
|
+
import { expr } from "./expr.js";
|
|
3
|
+
import { atom, bare, operand, strArg, UNARY } from "./parens.js";
|
|
4
|
+
import { Emitter } from "./emitter.js";
|
|
5
|
+
import { rustTy } from "./rust.js";
|
|
6
|
+
import { fieldInit } from "./children.js";
|
|
7
|
+
import { pushesContent, statements } from "./template.js";
|
|
8
|
+
function borrowed(code) {
|
|
9
|
+
return code.startsWith("&") || /^\w+$/.test(code) || /^fv_sp\d+\.\w+$/.test(code) ? code : `&${operand(code, UNARY)}`;
|
|
10
|
+
}
|
|
11
|
+
export function slotBody(s, value) {
|
|
12
|
+
return slotContent(s, value).body;
|
|
13
|
+
}
|
|
14
|
+
export function slotContent(s, value) {
|
|
15
|
+
const fn = value.type === "CallExpression" && value.callee.type === "Identifier" && value.callee.name === "_withCtx"
|
|
16
|
+
? value.arguments[0]
|
|
17
|
+
: null;
|
|
18
|
+
if (fn?.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
|
|
19
|
+
fail(s.comp, "unexpected slot content", value);
|
|
20
|
+
}
|
|
21
|
+
const branch = fn.body.body.length === 1 ? fn.body.body[0] : null;
|
|
22
|
+
if (branch?.type !== "IfStatement" || branch.test.type !== "Identifier" || branch.test.name !== "_push") {
|
|
23
|
+
fail(s.comp, "unexpected slot content", fn);
|
|
24
|
+
}
|
|
25
|
+
const body = branch.consequent.type === "BlockStatement" ? branch.consequent.body : [branch.consequent];
|
|
26
|
+
return { body, param: fn.params[0] };
|
|
27
|
+
}
|
|
28
|
+
export function staticallyFilled(body) {
|
|
29
|
+
return body.some((st) => st.type === "ExpressionStatement" && st.expression.type === "CallExpression" &&
|
|
30
|
+
st.expression.callee.type === "Identifier" && st.expression.callee.name === "_push" &&
|
|
31
|
+
pushesContent(st.expression.arguments[0]) === true);
|
|
32
|
+
}
|
|
33
|
+
export function slotTypeName(slotName, suffix) {
|
|
34
|
+
return slotName.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + suffix;
|
|
35
|
+
}
|
|
36
|
+
export function slotFieldBorrows(ty) {
|
|
37
|
+
const copied = (t) => t.k === "int" || t.k === "float" || t.k === "bool";
|
|
38
|
+
return !(copied(ty) || (ty.k === "opt" && copied(ty.of)));
|
|
39
|
+
}
|
|
40
|
+
export function slotFieldTy(ty, comp) {
|
|
41
|
+
const owned = (t) => rustTy(t, comp).replace(/'a\b/g, "'v");
|
|
42
|
+
switch (ty.k) {
|
|
43
|
+
case "int":
|
|
44
|
+
return "i64";
|
|
45
|
+
case "float":
|
|
46
|
+
return "f64";
|
|
47
|
+
case "bool":
|
|
48
|
+
return "bool";
|
|
49
|
+
case "str":
|
|
50
|
+
return "&'v str";
|
|
51
|
+
case "list":
|
|
52
|
+
return `&'v [${owned(ty.of)}]`;
|
|
53
|
+
case "struct":
|
|
54
|
+
case "child":
|
|
55
|
+
case "html":
|
|
56
|
+
return `&'v ${owned(ty)}`;
|
|
57
|
+
case "opt":
|
|
58
|
+
return `Option<${slotFieldTy(ty.of, comp)}>`;
|
|
59
|
+
default:
|
|
60
|
+
throw new GenError(`no Rust type for a slot prop of type ${JSON.stringify(ty)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function slotFieldValue(s, v, n) {
|
|
64
|
+
switch (v.ty.k) {
|
|
65
|
+
case "str":
|
|
66
|
+
return strArg(v.code);
|
|
67
|
+
case "int":
|
|
68
|
+
case "float":
|
|
69
|
+
case "bool":
|
|
70
|
+
return bare(v.code);
|
|
71
|
+
case "list":
|
|
72
|
+
if (v.code.startsWith("["))
|
|
73
|
+
fail(s.comp, "a slot prop is not an array literal: pass a list the component holds", n);
|
|
74
|
+
if (v.iter !== undefined)
|
|
75
|
+
fail(s.comp, "a slot prop is not a computed list: pass a list the component holds", n);
|
|
76
|
+
return borrowed(v.code);
|
|
77
|
+
case "struct":
|
|
78
|
+
case "child":
|
|
79
|
+
case "html":
|
|
80
|
+
return borrowed(v.code);
|
|
81
|
+
case "opt":
|
|
82
|
+
if (v.ty.of.k === "list")
|
|
83
|
+
return `${atom(v.code)}.map(|v| &v[..])`;
|
|
84
|
+
if (v.ty.of.k === "opt" || v.ty.of.k === "undef")
|
|
85
|
+
break;
|
|
86
|
+
return v.code;
|
|
87
|
+
}
|
|
88
|
+
return fail(s.comp, "a slot prop is a string, a number, a boolean, an object or a list", n);
|
|
89
|
+
}
|
|
90
|
+
export function slotOutlet(s, e, c) {
|
|
91
|
+
const [, nameNode, slotProps, fallback] = c.arguments;
|
|
92
|
+
if (nameNode?.type !== "StringLiteral")
|
|
93
|
+
fail(s.comp, "a slot's name is literal", c);
|
|
94
|
+
if (slotProps?.type !== "ObjectExpression")
|
|
95
|
+
fail(s.comp, "a slot's props are attributes or a `v-bind` object literal", c);
|
|
96
|
+
const slotName = nameNode.value;
|
|
97
|
+
const field = `fv_slots.${snake(slotName)}`;
|
|
98
|
+
const outlet = `\`<slot${slotName === "default" ? "" : ` name="${slotName}"`}>\``;
|
|
99
|
+
let fn = "fv::slot_into";
|
|
100
|
+
let args = field;
|
|
101
|
+
if (slotProps.properties.length) {
|
|
102
|
+
const fields = [];
|
|
103
|
+
const values = [];
|
|
104
|
+
for (const p of slotProps.properties) {
|
|
105
|
+
if (p.type !== "ObjectProperty" || p.computed)
|
|
106
|
+
fail(s.comp, "slot props hold plain names", p);
|
|
107
|
+
const key = p.key.name ?? p.key.value;
|
|
108
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key))
|
|
109
|
+
fail(s.comp, `slot prop \`${key}\` is not a plain name; write it in camelCase`, p);
|
|
110
|
+
const v = expr(s, p.value);
|
|
111
|
+
fields.push({ js: key, rust: snake(key), ty: v.ty });
|
|
112
|
+
values.push(fieldInit(snake(key), slotFieldValue(s, v, p.value)));
|
|
113
|
+
}
|
|
114
|
+
const name = slotTypeName(slotName, "SlotProps");
|
|
115
|
+
const shape = { name, fields, slot: true };
|
|
116
|
+
const had = s.comp.slotShapes.get(slotName);
|
|
117
|
+
if (had && JSON.stringify(had.fields) !== JSON.stringify(fields)) {
|
|
118
|
+
fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
|
|
119
|
+
}
|
|
120
|
+
if (!had) {
|
|
121
|
+
if (s.comp.structs.has(name) && !s.comp.structs.get(name).slot)
|
|
122
|
+
fail(s.comp, `\`${name}\` names this slot's props; rename the interface`, c);
|
|
123
|
+
s.comp.slotShapes.set(slotName, shape);
|
|
124
|
+
s.comp.structs.set(name, shape);
|
|
125
|
+
}
|
|
126
|
+
fn = "fv::scoped_slot_into";
|
|
127
|
+
args = `${field}, &${name} { ${values.join(", ")} }`;
|
|
128
|
+
}
|
|
129
|
+
else if (s.comp.slotShapes.has(slotName)) {
|
|
130
|
+
fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
|
|
131
|
+
}
|
|
132
|
+
const id = c.arguments[6];
|
|
133
|
+
let slotted = null;
|
|
134
|
+
if (id?.type === "StringLiteral")
|
|
135
|
+
slotted = rustStr(id.value);
|
|
136
|
+
else if (id?.type === "BinaryExpression" && id.operator === "+" && id.left.type === "StringLiteral" && id.right.type === "Identifier" && id.right.name === "_scopeId") {
|
|
137
|
+
slotted = s.sid === null ? rustStr(id.left.value) : `&[${rustStr(id.left.value)}, ${s.sid}].concat()`;
|
|
138
|
+
}
|
|
139
|
+
else if (id?.type === "Identifier" && id.name === "_scopeId")
|
|
140
|
+
slotted = s.sid;
|
|
141
|
+
else if (id && id.type !== "NullLiteral")
|
|
142
|
+
fail(s.comp, "unexpected slot scope id", id);
|
|
143
|
+
if (s.comp.passesSlotIds) {
|
|
144
|
+
fn = fn === "fv::slot_into" ? "fv::slot_into_slotted" : "fv::scoped_slot_into_slotted";
|
|
145
|
+
args += `, ${slotted ?? '""'}`;
|
|
146
|
+
}
|
|
147
|
+
else if (slotted !== null)
|
|
148
|
+
fail(s.comp, "a slot scope id passed to content that does not take one", c);
|
|
149
|
+
const call = (rest) => (s.fill ? `if ${fn}(out, ${args}, ${rest}) { filled = true; }` : `${fn}(out, ${args}, ${rest});`);
|
|
150
|
+
if (fallback?.type === "NullLiteral" || !fallback) {
|
|
151
|
+
e.stmt(call("None"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (fallback.type !== "ArrowFunctionExpression" || fallback.body.type !== "BlockStatement") {
|
|
155
|
+
fail(s.comp, "unexpected slot fallback", fallback);
|
|
156
|
+
}
|
|
157
|
+
e.open(`${s.fill ? "if " : ""}${fn}(out, ${args}, Some(&mut |out: &mut String|`);
|
|
158
|
+
statements(s, e, fallback.body.body);
|
|
159
|
+
e.close(s.fill ? ")) { filled = true; }" : "));");
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Component, type N, type Scope, type Val } from "./model.ts";
|
|
2
|
+
export declare function lonely(what: string): string;
|
|
3
|
+
export declare function meet(comp: Component, a: Val, b: Val, what: string, n: N, how: "equal" | "order" | "search" | "join"): void;
|
|
4
|
+
export declare function loneOf(...vs: (Val | undefined)[]): {
|
|
5
|
+
lone?: true;
|
|
6
|
+
};
|
|
7
|
+
export declare function isTemporary(v: Val): boolean;
|
|
8
|
+
export declare function asCow(v: Val): string;
|
|
9
|
+
export declare function yieldsCow(code: string): boolean;
|
|
10
|
+
export declare function unquote(literal: string): string;
|
|
11
|
+
export declare function stringsEqual(a: Val, b: Val): string;
|
|
12
|
+
export declare function formatted(comp: Component, parts: (string | Val)[], n: N, what: string): Val;
|
|
13
|
+
export declare function stringMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
|
package/dist/strings.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { BOOL, fail, INT, opt, rustStr, STR } from "./model.js";
|
|
2
|
+
import { atom, bare, binary, condition, enclosed, receiver, strArg } from "./parens.js";
|
|
3
|
+
import { computed } from "./lists.js";
|
|
4
|
+
import { expr } from "./expr.js";
|
|
5
|
+
import { known } from "./narrowing.js";
|
|
6
|
+
import { asF64, isNumber } from "./numbers.js";
|
|
7
|
+
export function lonely(what) {
|
|
8
|
+
return `${what} with a string that may hold half of a surrogate pair (cut by \`slice\`, \`substring\`, \`at\`, \`charAt\`, \`split("")\` and the like): JavaScript keeps the half, which can match, order or join with another where ferrovue holds U+FFFD`;
|
|
9
|
+
}
|
|
10
|
+
export function meet(comp, a, b, what, n, how) {
|
|
11
|
+
const literal = (v) => v.code.startsWith('"');
|
|
12
|
+
const fffd = (v) => literal(v) && v.code.includes("\u{FFFD}");
|
|
13
|
+
const low = (v) => literal(v) && !/[\u{D800}-\u{10FFFF}]/u.test(v.code);
|
|
14
|
+
const refused = how === "order" ? (a.lone && !low(b)) || (b.lone && !low(a))
|
|
15
|
+
: how === "join" ? a.lone && b.lone
|
|
16
|
+
: how === "search" ? b.lone || (a.lone && fffd(b))
|
|
17
|
+
: (a.lone && b.lone) || (a.lone && fffd(b)) || (b.lone && fffd(a));
|
|
18
|
+
if (refused)
|
|
19
|
+
fail(comp, lonely(what), n);
|
|
20
|
+
}
|
|
21
|
+
export function loneOf(...vs) {
|
|
22
|
+
return vs.some((v) => v?.lone) ? { lone: true } : {};
|
|
23
|
+
}
|
|
24
|
+
function nonEmptyLiteral(v) {
|
|
25
|
+
return /^"[^"]/.test(v.code);
|
|
26
|
+
}
|
|
27
|
+
export function isTemporary(v) {
|
|
28
|
+
if (v.ty.k !== "str")
|
|
29
|
+
return false;
|
|
30
|
+
const trimmed = /^fv::js_trim(?:_start|_end)?\(([^]*)\)$/.exec(v.code);
|
|
31
|
+
if (trimmed?.[1] !== undefined)
|
|
32
|
+
return isTemporary({ code: trimmed[1].replace(/^&(?!\*)/, "&*"), ty: v.ty });
|
|
33
|
+
return /^&\*(?!\**[\w.]+$)/.test(v.code) || /\.unwrap_or\(&\*(?!\**[\w.]+\)$)/.test(v.code);
|
|
34
|
+
}
|
|
35
|
+
export function asCow(v) {
|
|
36
|
+
if (!isTemporary(v))
|
|
37
|
+
return `std::borrow::Cow::<str>::Borrowed(${bare(v.code)})`;
|
|
38
|
+
const built = /^&\*(.*)$/s.exec(v.code)?.[1];
|
|
39
|
+
if (built === undefined)
|
|
40
|
+
return `std::borrow::Cow::<str>::Owned(${atom(v.code)}.to_owned())`;
|
|
41
|
+
if (enclosed(built))
|
|
42
|
+
return bare(built);
|
|
43
|
+
if (yieldsCow(built))
|
|
44
|
+
return built.startsWith("fv::") ? `std::borrow::Cow::<str>::Owned(${built}.into_owned())` : bare(built);
|
|
45
|
+
if (/^fv::js_(?:char_at|trim|trim_start|trim_end)\(/.test(built))
|
|
46
|
+
return `std::borrow::Cow::<str>::Owned(${built}.to_owned())`;
|
|
47
|
+
return `std::borrow::Cow::<str>::Owned(${built})`;
|
|
48
|
+
}
|
|
49
|
+
export function yieldsCow(code) {
|
|
50
|
+
const routine = /^fv::js_(?:slice|substring|replace|replace_all|pad_start|pad_end)(?=\()/.exec(code);
|
|
51
|
+
if (routine !== null)
|
|
52
|
+
return enclosed(code.slice(routine[0].length));
|
|
53
|
+
const fallback = code.lastIndexOf(".unwrap_or(std::borrow::Cow::<str>::");
|
|
54
|
+
return fallback >= 0 && enclosed(code.slice(fallback + ".unwrap_or".length));
|
|
55
|
+
}
|
|
56
|
+
export function unquote(literal) {
|
|
57
|
+
const escapes = { n: "\n", r: "\r", t: "\t" };
|
|
58
|
+
return literal.slice(1, -1).replace(/\\(?:u\{([0-9a-f]+)\}|(.))/g, (_, hex, c) => (hex ? String.fromCodePoint(parseInt(hex, 16)) : (escapes[c] ?? c)));
|
|
59
|
+
}
|
|
60
|
+
export function stringsEqual(a, b) {
|
|
61
|
+
if (known(a) !== undefined && known(b) !== undefined)
|
|
62
|
+
return String(unquote(a.code) === unquote(b.code));
|
|
63
|
+
if (a.code === '""' || b.code === '""')
|
|
64
|
+
return `${receiver(a.code === '""' ? b.code : a.code)}.is_empty()`;
|
|
65
|
+
return binary(a.code, "==", b.code);
|
|
66
|
+
}
|
|
67
|
+
export function formatted(comp, parts, n, what) {
|
|
68
|
+
let fmt = "";
|
|
69
|
+
const args = [];
|
|
70
|
+
let before;
|
|
71
|
+
const lone = loneOf(...parts.map((p) => (typeof p === "string" ? undefined : p)));
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (typeof part === "string" || (part.ty.k === "str" && known(part) !== undefined)) {
|
|
74
|
+
const text = typeof part === "string" ? part : unquote(part.code);
|
|
75
|
+
if (text !== "")
|
|
76
|
+
before = undefined;
|
|
77
|
+
fmt += text.replace(/[{}]/g, (c) => c + c);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (before)
|
|
81
|
+
meet(comp, before, part, what, n, "join");
|
|
82
|
+
before = part;
|
|
83
|
+
if (part.format) {
|
|
84
|
+
fmt += part.format.text;
|
|
85
|
+
args.push(...part.format.args);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (part.ty.k === "str")
|
|
89
|
+
args.push(receiver(part.code).replace(/^(fv::Js\(.*\))\.to_string\(\)$/s, "$1"));
|
|
90
|
+
else if (isNumber(part.ty))
|
|
91
|
+
args.push(`fv::Js(${bare(part.code)})`);
|
|
92
|
+
else if (part.ty.k === "bool")
|
|
93
|
+
args.push(`if ${condition(part.code)} { "true" } else { "false" }`);
|
|
94
|
+
else
|
|
95
|
+
fail(comp, "a template literal interpolates strings, numbers and booleans that are present", n);
|
|
96
|
+
fmt += "{}";
|
|
97
|
+
}
|
|
98
|
+
if (!args.length)
|
|
99
|
+
return { code: rustStr(fmt.replace(/\{\{|\}\}/g, (c) => c[0])), ty: STR };
|
|
100
|
+
const one = parts.filter((p) => (typeof p === "string" ? p !== "" : p.code !== '""'));
|
|
101
|
+
if (fmt === "{}" && one.length === 1 && typeof one[0] !== "string" && one[0].ty.k === "str")
|
|
102
|
+
return one[0];
|
|
103
|
+
return { code: `&*format!(${rustStr(fmt)}, ${args.join(", ")})`, ty: STR, format: { text: fmt, args }, ...lone };
|
|
104
|
+
}
|
|
105
|
+
export function stringMethod(s, target, method, args, n) {
|
|
106
|
+
const comp = s.comp;
|
|
107
|
+
const t = strArg(target.code);
|
|
108
|
+
const arity = (least, most) => {
|
|
109
|
+
if (args.length < least || args.length > most) {
|
|
110
|
+
fail(comp, `\`.${method}()\` takes ${least === most ? least : `${least} to ${most}`} argument${most === 1 ? "" : "s"} here`, n);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const str = (i) => {
|
|
114
|
+
if (args[i]?.type === "RegExpLiteral")
|
|
115
|
+
fail(comp, `\`.${method}()\` with a regular expression, which the server does not run: give it a string`, args[i]);
|
|
116
|
+
const v = expr(s, args[i]);
|
|
117
|
+
if (v.ty.k !== "str")
|
|
118
|
+
fail(comp, `\`.${method}()\` takes a string that is present`, args[i]);
|
|
119
|
+
return v;
|
|
120
|
+
};
|
|
121
|
+
const num = (i, fallback) => {
|
|
122
|
+
if (args[i] === undefined)
|
|
123
|
+
return fallback;
|
|
124
|
+
const v = expr(s, args[i]);
|
|
125
|
+
if (v.ty.k === "undef")
|
|
126
|
+
return fallback;
|
|
127
|
+
if (!isNumber(v.ty))
|
|
128
|
+
fail(comp, `\`.${method}()\` takes a number that is present`, args[i]);
|
|
129
|
+
return asF64(v);
|
|
130
|
+
};
|
|
131
|
+
const cut = { lone: true };
|
|
132
|
+
switch (method) {
|
|
133
|
+
case "includes":
|
|
134
|
+
case "startsWith":
|
|
135
|
+
case "endsWith": {
|
|
136
|
+
arity(1, 1);
|
|
137
|
+
const x = str(0);
|
|
138
|
+
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
139
|
+
const rust = { includes: "contains", startsWith: "starts_with", endsWith: "ends_with" }[method];
|
|
140
|
+
return { code: `${receiver(target.code)}.${rust}(${bare(x.code)})`, ty: BOOL };
|
|
141
|
+
}
|
|
142
|
+
case "indexOf":
|
|
143
|
+
case "lastIndexOf": {
|
|
144
|
+
arity(1, 1);
|
|
145
|
+
const x = str(0);
|
|
146
|
+
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
147
|
+
return { code: `fv::js_${method === "indexOf" ? "index_of" : "last_index_of"}(${t}, ${strArg(x.code)})`, ty: INT };
|
|
148
|
+
}
|
|
149
|
+
case "slice":
|
|
150
|
+
case "substring": {
|
|
151
|
+
arity(0, 2);
|
|
152
|
+
const end = num(1, "");
|
|
153
|
+
return { code: `&*fv::js_${method}(${t}, ${num(0, "0.0")}, ${end ? `Some(${end})` : "None"})`, ty: STR, ...cut };
|
|
154
|
+
}
|
|
155
|
+
case "at": {
|
|
156
|
+
arity(1, 1);
|
|
157
|
+
const at = `fv::js_at(${t}, ${num(0, "0.0")})`;
|
|
158
|
+
if (!isTemporary(target))
|
|
159
|
+
return { code: at, ty: opt(STR), ...cut };
|
|
160
|
+
const held = `${at}.map(|v| std::borrow::Cow::<str>::Owned(v.to_owned()))`;
|
|
161
|
+
return { code: `${atom(held)}.as_deref()`, ty: opt(STR), held, ...cut };
|
|
162
|
+
}
|
|
163
|
+
case "charAt":
|
|
164
|
+
arity(0, 1);
|
|
165
|
+
return { code: `${isTemporary(target) ? "&*" : ""}fv::js_char_at(${t}, ${num(0, "0.0")})`, ty: STR, ...cut };
|
|
166
|
+
case "split": {
|
|
167
|
+
arity(1, 1);
|
|
168
|
+
const sep = str(0);
|
|
169
|
+
meet(comp, target, sep, "`.split()`", n, "search");
|
|
170
|
+
return computed(`fv::js_split(${t}, ${strArg(sep.code)}).into_iter()`, STR, target.lone || !nonEmptyLiteral(sep));
|
|
171
|
+
}
|
|
172
|
+
case "replace":
|
|
173
|
+
case "replaceAll": {
|
|
174
|
+
arity(2, 2);
|
|
175
|
+
if (args[1].type === "ArrowFunctionExpression" || args[1].type === "FunctionExpression") {
|
|
176
|
+
fail(comp, `\`.${method}()\` with a function, which the server does not run: give it a string`, args[1]);
|
|
177
|
+
}
|
|
178
|
+
const [pattern, replacement] = [str(0), str(1)];
|
|
179
|
+
meet(comp, target, pattern, `\`.${method}()\``, n, "search");
|
|
180
|
+
meet(comp, target, replacement, `\`.${method}()\``, n, "join");
|
|
181
|
+
const lone = target.lone || replacement.lone || (method === "replaceAll" && !nonEmptyLiteral(pattern));
|
|
182
|
+
return { code: `&*fv::js_${method === "replace" ? "replace" : "replace_all"}(${t}, ${strArg(pattern.code)}, ${strArg(replacement.code)})`, ty: STR, ...(lone ? cut : {}) };
|
|
183
|
+
}
|
|
184
|
+
case "padStart":
|
|
185
|
+
case "padEnd": {
|
|
186
|
+
arity(1, 2);
|
|
187
|
+
const fill = args.length > 1 ? str(1) : { code: '" "', ty: STR };
|
|
188
|
+
const whole = fill.code.startsWith('"') && !/[\u{10000}-\u{10FFFF}]/u.test(fill.code);
|
|
189
|
+
if (fill.lone || (method === "padStart" && target.lone && !whole))
|
|
190
|
+
fail(comp, lonely(`\`.${method}()\``), n);
|
|
191
|
+
return { code: `&*fv::js_${method === "padStart" ? "pad_start" : "pad_end"}(${t}, ${num(0, "0.0")}, ${strArg(fill.code)})`, ty: STR, ...(target.lone || !whole ? cut : {}) };
|
|
192
|
+
}
|
|
193
|
+
case "repeat": {
|
|
194
|
+
arity(1, 1);
|
|
195
|
+
const c = args[0];
|
|
196
|
+
if ((c.type === "UnaryExpression" && c.operator === "-" && c.argument.type === "NumericLiteral" && c.argument.value > 0) || (c.type === "Identifier" && c.name === "Infinity")) {
|
|
197
|
+
fail(comp, "`.repeat()` with a negative or infinite count, which throws a `RangeError`", c);
|
|
198
|
+
}
|
|
199
|
+
if (target.lone)
|
|
200
|
+
fail(comp, lonely("`.repeat()`"), n);
|
|
201
|
+
return { code: `&*fv::js_repeat(${t}, ${num(0, "0.0")})`, ty: STR };
|
|
202
|
+
}
|
|
203
|
+
case "toLocaleUpperCase":
|
|
204
|
+
case "toLocaleLowerCase":
|
|
205
|
+
return fail(comp, `\`.${method}()\` maps case by the locale the server runs in, which the browser need not share: use \`.${method.replace("Locale", "")}()\``, n);
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|