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/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,21 +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
|
-
attrs: comp.inherits ? "fv_attrs" : null,
|
|
85
|
+
attrs: comp.inherits ? (takesAttrs(comp) ? "fv_attrs.ids()" : "fv_attrs") : null,
|
|
86
|
+
fallthrough: takesAttrs(comp) ? "fv_attrs" : null,
|
|
87
|
+
attrsBindings: new Set(),
|
|
96
88
|
sid: null,
|
|
89
|
+
plugins: new Map(),
|
|
97
90
|
};
|
|
91
|
+
for (const p of ctx.plugins)
|
|
92
|
+
if (p.scope)
|
|
93
|
+
scope.plugins.set(p, p.scope(scope));
|
|
98
94
|
const lets = [];
|
|
99
|
-
|
|
100
|
-
let storeToRefsName = null;
|
|
101
|
-
let useRouteName = null;
|
|
102
|
-
let useI18nName = null;
|
|
103
|
-
/** Setup bindings holding a store, by name. */
|
|
104
|
-
const storeValues = new Map();
|
|
95
|
+
let useAttrsName = null;
|
|
105
96
|
for (const st of ast) {
|
|
106
97
|
if (st.type === "ImportDeclaration") {
|
|
107
98
|
const from = st.source.value;
|
|
@@ -109,46 +100,14 @@ export function scopeFor(comp, ast, components) {
|
|
|
109
100
|
const def = st.specifiers.find((x) => x.type === "ImportDefaultSpecifier");
|
|
110
101
|
if (def)
|
|
111
102
|
scope.children.set(def.local.name, basename(from, ".vue"));
|
|
103
|
+
continue;
|
|
112
104
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// A type from the store's file, which `readComponent` has already resolved.
|
|
117
|
-
if (st.importKind === "type" || sp.importKind === "type" || (hook !== null && ctx.storeStructs.has(hook)))
|
|
118
|
-
continue;
|
|
119
|
-
const store = hook ? ctx.stores.get(hook) : undefined;
|
|
120
|
-
if (!store || store.module !== storeImport(comp, from))
|
|
121
|
-
fail(comp, "import a store by its `use…` hook", sp);
|
|
122
|
-
storeHooks.set(sp.local.name, store);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else if (from === "vue-router") {
|
|
126
|
-
for (const sp of st.specifiers) {
|
|
127
|
-
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
128
|
-
if (name === "useRoute")
|
|
129
|
-
useRouteName = sp.local.name;
|
|
130
|
-
else if (name === "RouterLink" || name === "RouterView")
|
|
131
|
-
scope.router.set(sp.local.name, name);
|
|
132
|
-
else
|
|
133
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-router does not run on the server`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else if (from === "vue-i18n") {
|
|
137
|
-
for (const sp of st.specifiers) {
|
|
138
|
-
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
139
|
-
if (name === "useI18n")
|
|
140
|
-
useI18nName = sp.local.name;
|
|
141
|
-
else
|
|
142
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-i18n does not run on the server`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
else if (from === "pinia") {
|
|
105
|
+
if (ctx.plugins.some((p) => p.scriptImport?.(scope, st, from)))
|
|
106
|
+
continue;
|
|
107
|
+
if (from === "vue") {
|
|
146
108
|
for (const sp of st.specifiers) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
storeToRefsName = sp.local.name;
|
|
150
|
-
else
|
|
151
|
-
scope.clientOnly.set(sp.local.name, `\`${name}\` from Pinia does not run on the server`);
|
|
109
|
+
if (sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === "useAttrs")
|
|
110
|
+
useAttrsName = sp.local.name;
|
|
152
111
|
}
|
|
153
112
|
}
|
|
154
113
|
else if (ctx.helperModule !== null && from === ctx.helperModule) {
|
|
@@ -172,14 +131,10 @@ export function scopeFor(comp, ast, components) {
|
|
|
172
131
|
setupStatement(comp, st);
|
|
173
132
|
continue;
|
|
174
133
|
}
|
|
175
|
-
// Setup runs on the server too, so a statement there can change what renders: `x.value = ...`
|
|
176
|
-
// after a `ref`, an `if`, a loop. None of it is translated, so none of it may exist.
|
|
177
134
|
if (st.type !== "VariableDeclaration")
|
|
178
135
|
fail(comp, `\`${st.type}\` in setup is not supported`, st);
|
|
179
136
|
for (const d of st.declarations) {
|
|
180
137
|
const init0 = d.init;
|
|
181
|
-
// `const { size = "md", label: l } = defineProps<...>()`: each name reads that prop, whose
|
|
182
|
-
// default `compileScript` already resolved.
|
|
183
138
|
if (d.id.type === "ObjectPattern" && definePropsType(init0)) {
|
|
184
139
|
for (const p of d.id.properties) {
|
|
185
140
|
if (p.type !== "ObjectProperty" || p.computed)
|
|
@@ -192,67 +147,19 @@ export function scopeFor(comp, ast, components) {
|
|
|
192
147
|
}
|
|
193
148
|
continue;
|
|
194
149
|
}
|
|
195
|
-
|
|
196
|
-
// `const { t, locale } = useI18n()`: \`t\` translates, \`locale\` is the request's locale.
|
|
197
|
-
if (d.id.type === "ObjectPattern" && calls(useI18nName)) {
|
|
198
|
-
for (const p of d.id.properties) {
|
|
199
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
200
|
-
fail(comp, "`useI18n()` is destructured into plain names", p);
|
|
201
|
-
}
|
|
202
|
-
const key = p.key.name ?? p.key.value;
|
|
203
|
-
if (key === "t")
|
|
204
|
-
scope.i18nT.add(p.value.name);
|
|
205
|
-
else if (key === "locale") {
|
|
206
|
-
scope.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
|
|
207
|
-
scope.refs.add(p.value.name);
|
|
208
|
-
}
|
|
209
|
-
else
|
|
210
|
-
scope.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
|
|
211
|
-
}
|
|
212
|
-
comp.readsI18n = true;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
// `const { density, label: l } = storeToRefs(prefs)`: each name is that field of the state.
|
|
216
|
-
if (d.id.type === "ObjectPattern" && calls(storeToRefsName)) {
|
|
217
|
-
const arg = init0.arguments[0];
|
|
218
|
-
const store = arg?.type === "Identifier" ? storeValues.get(arg.name) : undefined;
|
|
219
|
-
if (!store || init0.arguments.length !== 1)
|
|
220
|
-
fail(comp, "`storeToRefs` takes a store bound in this setup", d);
|
|
221
|
-
for (const p of d.id.properties) {
|
|
222
|
-
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
223
|
-
fail(comp, "`storeToRefs` is destructured into plain names", p);
|
|
224
|
-
}
|
|
225
|
-
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
226
|
-
const state = { code: `fv_stores.${store.field}`, ty: { k: "struct", name: store.state, store: true } };
|
|
227
|
-
scope.setup.set(p.value.name, storeGetter(scope, state, key, p) ?? fieldVal(comp, state.code, state.ty, key, p));
|
|
228
|
-
scope.refs.add(p.value.name);
|
|
229
|
-
}
|
|
230
|
-
comp.readsStores = true;
|
|
150
|
+
if (ctx.plugins.some((p) => p.scriptBinding?.(scope, d)))
|
|
231
151
|
continue;
|
|
232
|
-
}
|
|
233
152
|
if (d.id.type !== "Identifier") {
|
|
234
|
-
// Destructuring anything else: client-side state the template may not read.
|
|
235
153
|
for (const name of patternNames(d.id))
|
|
236
154
|
scope.clientOnly.set(name, "it is destructured from a value the server does not have");
|
|
237
155
|
continue;
|
|
238
156
|
}
|
|
239
157
|
const local = d.id.name;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (init0.arguments.length)
|
|
244
|
-
fail(comp, "a store hook takes no arguments", d);
|
|
245
|
-
storeValues.set(local, hook);
|
|
246
|
-
scope.setup.set(local, { code: `fv_stores.${hook.field}`, ty: { k: "struct", name: hook.state, store: true } });
|
|
247
|
-
comp.readsStores = true;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
// `const route = useRoute()`: the reader's location.
|
|
251
|
-
if (useRouteName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useRouteName) {
|
|
252
|
-
scope.setup.set(local, theRoute(scope, d));
|
|
158
|
+
if (useAttrsName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useAttrsName) {
|
|
159
|
+
scope.attrsBindings.add(local);
|
|
160
|
+
scope.clientOnly.set(local, "`useAttrs()` is bound whole, with `v-bind`; a value read from it has no type");
|
|
253
161
|
continue;
|
|
254
162
|
}
|
|
255
|
-
// `const model = defineModel<string>()`: the model's prop.
|
|
256
163
|
const model = comp.models.get(local);
|
|
257
164
|
if (model !== undefined) {
|
|
258
165
|
scope.setup.set(local, fieldVal(comp, "props", { k: "struct", name: "Props" }, model, d));
|
|
@@ -275,9 +182,6 @@ export function scopeFor(comp, ast, components) {
|
|
|
275
182
|
scope.clientOnly.set(local, "it is not a value the server computes: only `ref(…)`, `computed(() => …)` and plain expressions are");
|
|
276
183
|
continue;
|
|
277
184
|
}
|
|
278
|
-
// Evaluated once, as setup is on the server. A value the server cannot evaluate is client-side
|
|
279
|
-
// state — a template ref, an element, a `reactive` object — which the template may still name
|
|
280
|
-
// from an event handler; a reference the server needs fails in `expr` with this reason.
|
|
281
185
|
let v;
|
|
282
186
|
try {
|
|
283
187
|
v = expr(scope, source);
|
|
@@ -288,25 +192,22 @@ export function scopeFor(comp, ast, components) {
|
|
|
288
192
|
scope.clientOnly.set(local, e.message.replace(/^[^:]*: /, ""));
|
|
289
193
|
continue;
|
|
290
194
|
}
|
|
291
|
-
if (v.ty.k === "undef") {
|
|
195
|
+
if (v.ty.k === "undef" || (v.ty.k === "str" && known(v) !== undefined)) {
|
|
292
196
|
scope.setup.set(local, v);
|
|
293
197
|
continue;
|
|
294
198
|
}
|
|
295
199
|
const name = `s_${snake(local).replace(/^r#/, "")}`;
|
|
296
200
|
const lone = v.lone ? { lone: true } : {};
|
|
297
|
-
// A computed list is kept as its items, and read again by item.
|
|
298
201
|
if (v.ty.k === "list" && v.iter !== undefined) {
|
|
299
202
|
lets.push(`let ${name} = ${collected(v)};`);
|
|
300
203
|
scope.setup.set(local, heldList(name, v.ty.of, v.lone));
|
|
301
204
|
continue;
|
|
302
205
|
}
|
|
303
|
-
// An optional string a temporary owns is kept as that `Option<Cow>`.
|
|
304
206
|
if (v.held !== undefined) {
|
|
305
207
|
lets.push(`let ${name} = ${v.held};`);
|
|
306
208
|
scope.setup.set(local, { code: `${name}.as_deref()`, ty: v.ty, ...lone });
|
|
307
209
|
continue;
|
|
308
210
|
}
|
|
309
|
-
// A list, a record or an object is a place in the props: borrowed, never moved out of them.
|
|
310
211
|
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
311
212
|
lets.push(`let ${name} = ${place ? `&${operand(v.code, UNARY)}` : bare(v.code)};`);
|
|
312
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
|
+
}
|