ferrovue 0.1.0 → 0.2.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.
@@ -0,0 +1,38 @@
1
+ import { type App, type Component } from "vue";
2
+ import { type RouteRecordRaw } from "vue-router";
3
+ export interface Fixture {
4
+ props: Record<string, unknown>;
5
+ slots: Record<string, string>;
6
+ route: string;
7
+ stores: Record<string, unknown>;
8
+ /** `$locale`: the locale the fixture renders in, when the project translates. */
9
+ locale?: string | null;
10
+ }
11
+ /** A fixture's JSON, split into the props and the rest. */
12
+ export declare function readFixture(json: Record<string, unknown>): Fixture;
13
+ /** A route as a routes file lists it: a path, or a path and a name. */
14
+ export type RouteEntry = string | {
15
+ path: string;
16
+ name?: string;
17
+ children?: RouteEntry[];
18
+ };
19
+ /** The router options ferrovue reproduces, as the project's configuration gives them. */
20
+ export interface RouterOptions {
21
+ base?: string;
22
+ linkActiveClass?: string;
23
+ linkExactActiveClass?: string;
24
+ /** vue-i18n, when the project translates: every locale's messages, the default locale, and the
25
+ * fallbacks. */
26
+ i18n?: {
27
+ messages: Record<string, unknown>;
28
+ locale: string;
29
+ fallbackLocale?: string | string[];
30
+ };
31
+ /** Render on the client with `createApp`, rather than hydrate what the server rendered: the scope
32
+ * ids a fresh client render writes, which the server's must equal for scoped styles to apply. */
33
+ client?: boolean;
34
+ }
35
+ /** Route records for vue-router, every route — nested ones too — given the fixture's view. */
36
+ export declare function routeRecords(routes: RouteEntry[], View: Component): RouteRecordRaw[];
37
+ /** An app rendering one fixture of `component`: with a router over `routes` when there are any. */
38
+ export declare function fixtureApp(component: Component, fixture: Fixture, routes: RouteEntry[] | null, options?: RouterOptions): Promise<App>;
@@ -0,0 +1,78 @@
1
+ /* An app rendering one conformance fixture with the real Vue: its props, its slots' content, the
2
+ * location it is rendered at under the real vue-router, Pinia's state and vue-i18n's locale.
3
+ *
4
+ * A fixture is a JSON object of props, plus keys that are not props:
5
+ * - `$slots`: each slot's content as HTML, rendered as a static node so both sides write it
6
+ * exactly as given; `routerView` is the page `<RouterView>` shows.
7
+ * - `$route`: the reader's location, `/` when absent.
8
+ * - `$stores`: Pinia's state, by store id — every store the component reads, in full, because a
9
+ * store absent here is built from its own `state()`, which the server never runs.
10
+ * - `$locale`: the locale, when the project translates.
11
+ *
12
+ * Nothing here reads a file or compiles anything, so a browser bundle can import it too: the
13
+ * browser hydration tests (`packages/ferrovue/browser/`) build the same app there. */
14
+ import { createApp, createSSRApp, createStaticVNode, defineComponent, h } from "vue";
15
+ import { createPinia } from "pinia";
16
+ import { createMemoryHistory, createRouter } from "vue-router";
17
+ import { createI18n } from "vue-i18n";
18
+ /** A fixture's JSON, split into the props and the rest. */
19
+ export function readFixture(json) {
20
+ const { $slots, $route, $stores, $locale, ...props } = json;
21
+ return {
22
+ props,
23
+ slots: ($slots ?? {}),
24
+ route: $route ?? "/",
25
+ stores: ($stores ?? {}),
26
+ locale: $locale ?? null,
27
+ };
28
+ }
29
+ /** The top-level nodes in a piece of HTML, which a static node needs to hydrate. */
30
+ function nodeCount(html) {
31
+ const t = document.createElement("template");
32
+ t.innerHTML = html;
33
+ return t.content.childNodes.length;
34
+ }
35
+ const staticNode = (html) => createStaticVNode(html, nodeCount(html));
36
+ /** Route records for vue-router, every route — nested ones too — given the fixture's view. */
37
+ export function routeRecords(routes, View) {
38
+ return routes.map((r) => typeof r === "string"
39
+ ? { path: r, component: View }
40
+ : { path: r.path, component: View, ...(r.name ? { name: r.name } : {}), ...(r.children ? { children: routeRecords(r.children, View) } : {}) });
41
+ }
42
+ /** An app rendering one fixture of `component`: with a router over `routes` when there are any. */
43
+ export async function fixtureApp(component, fixture, routes, options = {}) {
44
+ const slots = Object.fromEntries(Object.entries(fixture.slots)
45
+ .filter(([name]) => name !== "routerView")
46
+ .map(([name, html]) => [name, () => [staticNode(html)]]));
47
+ const app = (options.client ? createApp : createSSRApp)({ render: () => h(component, fixture.props, slots) });
48
+ // As the client hydrates: the state the server rendered with, set before any store is first used.
49
+ const pinia = createPinia();
50
+ pinia.state.value = structuredClone(fixture.stores);
51
+ app.use(pinia);
52
+ if (options.i18n) {
53
+ // Messages read from the project's files at run time: their schema is not known to TypeScript.
54
+ const i18nOptions = {
55
+ legacy: false,
56
+ locale: fixture.locale ?? options.i18n.locale,
57
+ ...(options.i18n.fallbackLocale !== undefined ? { fallbackLocale: options.i18n.fallbackLocale } : {}),
58
+ messages: options.i18n.messages,
59
+ missingWarn: false,
60
+ fallbackWarn: false,
61
+ };
62
+ app.use(createI18n(i18nOptions));
63
+ }
64
+ if (routes) {
65
+ const view = fixture.slots.routerView ?? "";
66
+ const View = defineComponent({ render: () => staticNode(view) });
67
+ const router = createRouter({
68
+ history: createMemoryHistory(options.base),
69
+ routes: routeRecords(routes, View),
70
+ ...(options.linkActiveClass !== undefined ? { linkActiveClass: options.linkActiveClass } : {}),
71
+ ...(options.linkExactActiveClass !== undefined ? { linkExactActiveClass: options.linkExactActiveClass } : {}),
72
+ });
73
+ app.use(router);
74
+ await router.push(fixture.route);
75
+ await router.isReady();
76
+ }
77
+ return app;
78
+ }
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
+ /** A locale's messages, each parsed by vue-i18n's own message compiler, by dotted key. */
3
+ export interface LocaleMessages {
4
+ name: string;
5
+ file: string;
6
+ messages: Map<string, N>;
7
+ }
8
+ /** The configured vue-i18n setup, read once per run. */
9
+ export interface I18nSetup {
10
+ /** The locale files' directory, as the configuration names it. */
11
+ dir: string;
12
+ locales: LocaleMessages[];
13
+ /** The locale a fixture renders in when it names none, and the fallbacks in order. */
14
+ locale: string;
15
+ fallback: string[];
16
+ }
17
+ /** Every message of every `*.json` locale file in the directory: nested objects joined with dots,
18
+ * as vue-i18n resolves a path — a nested key first, then a flat key of the same spelling. */
19
+ export declare function readLocales(root: string, config: {
20
+ messages: string;
21
+ locale?: string;
22
+ fallbackLocale?: string | string[];
23
+ }): I18nSetup;
24
+ export declare function i18nSource(setup: I18nSetup): string;
25
+ /** `t(key)`, `t(key, plural)`, `t(key, { named })`, `t(key, [list])`, `t(key, { named }, plural)`:
26
+ * the message translated in the request's locale. */
27
+ export declare function translate(s: Scope, args: N[], n: N): Val;
package/dist/i18n.js ADDED
@@ -0,0 +1,175 @@
1
+ /* vue-i18n: the locale files read and compiled to Rust tables, and `$t` / `useI18n().t` calls. */
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { createParser } from "@intlify/message-compiler";
5
+ import { fail, GenError, rustStr, STR } from "./model.js";
6
+ import { ctx } from "./context.js";
7
+ import { expr } from "./expr.js";
8
+ import { bare, strArg } from "./parens.js";
9
+ import { header } from "./rust.js";
10
+ /** The modifiers vue-i18n defines for linked messages; any other would throw in the browser. */
11
+ const MODIFIERS = new Set(["upper", "lower", "capitalize"]);
12
+ /** Every message of every `*.json` locale file in the directory: nested objects joined with dots,
13
+ * as vue-i18n resolves a path — a nested key first, then a flat key of the same spelling. */
14
+ export function readLocales(root, config) {
15
+ const dir = join(root, config.messages);
16
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json")).toSorted();
17
+ const parser = createParser({});
18
+ const locales = files.map((f) => {
19
+ const file = join(config.messages, f);
20
+ let data;
21
+ try {
22
+ data = JSON.parse(readFileSync(join(dir, f), "utf8"));
23
+ }
24
+ catch (e) {
25
+ throw new GenError(`${file}: ${e.message}`);
26
+ }
27
+ const messages = new Map();
28
+ const nested = new Map();
29
+ const flat = new Map();
30
+ const walk = (value, path) => {
31
+ if (typeof value === "string") {
32
+ const key = path.join(".");
33
+ // A key spelled with dots is reached only when no nested path is.
34
+ (path.length > 1 ? nested : flat).set(key, value);
35
+ }
36
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
37
+ for (const [k, v] of Object.entries(value))
38
+ walk(v, [...path, k]);
39
+ }
40
+ };
41
+ walk(data, []);
42
+ for (const [key, text] of [...flat, ...nested]) {
43
+ try {
44
+ messages.set(key, parser.parse(text));
45
+ }
46
+ catch (e) {
47
+ throw new GenError(`${file}: message \`${key}\`: ${e.message}`);
48
+ }
49
+ }
50
+ return { name: basename(f, ".json"), file, messages };
51
+ });
52
+ const fallback = config.fallbackLocale === undefined ? [] : Array.isArray(config.fallbackLocale) ? config.fallbackLocale : [config.fallbackLocale];
53
+ return { dir: config.messages, locales, locale: config.locale ?? "en", fallback };
54
+ }
55
+ /** A message's AST as the Rust `Message` literal: its cases, each a list of `Part`s. */
56
+ function messageSource(file, key, ast) {
57
+ const body = ast.body;
58
+ const cases = body.type === 1 ? body.cases : [body];
59
+ const part = (item) => {
60
+ switch (item.type) {
61
+ case 3:
62
+ return `Part::Text(${rustStr(item.value)})`;
63
+ case 4:
64
+ return `Part::Named(${rustStr(item.key)})`;
65
+ case 5:
66
+ return `Part::List(${item.index})`;
67
+ case 9:
68
+ return `Part::Literal(${rustStr(item.value)})`;
69
+ case 6: {
70
+ if (item.key?.type !== 7)
71
+ throw new GenError(`${file}: message \`${key}\` links to a key chosen at run time, which is not supported`);
72
+ const modifier = item.modifier?.value;
73
+ if (modifier !== undefined && !MODIFIERS.has(modifier)) {
74
+ throw new GenError(`${file}: message \`${key}\` uses the modifier \`${modifier}\`, which vue-i18n does not define`);
75
+ }
76
+ return `Part::Linked { key: ${rustStr(item.key.value)}, modifier: ${modifier === undefined ? "None" : `Some(${rustStr(modifier)})`} }`;
77
+ }
78
+ default:
79
+ throw new GenError(`${file}: message \`${key}\` holds a construct ferrovue does not render (node type ${item.type})`);
80
+ }
81
+ };
82
+ const items = (c) => c.items ?? (c.static !== undefined ? [{ type: 3, value: c.static }] : []);
83
+ return `Message { cases: &[${cases.map((c) => `&[${items(c).map(part).join(", ")}]`).join(", ")}] }`;
84
+ }
85
+ /** Keys in the order Rust's `str` sorts them — byte order — for the table's binary search. */
86
+ function byteOrder(a, b) {
87
+ return Buffer.compare(Buffer.from(a), Buffer.from(b));
88
+ }
89
+ export function i18nSource(setup) {
90
+ const locales = setup.locales
91
+ .map((l) => {
92
+ const keys = [...l.messages.keys()].toSorted(byteOrder);
93
+ const entries = keys.map((k) => ` (${rustStr(k)}, ${messageSource(l.file, k, l.messages.get(k))}),`).join("\n");
94
+ return ` Locale {
95
+ name: ${rustStr(l.name)},
96
+ messages: &[
97
+ ${entries}
98
+ ],
99
+ },`;
100
+ })
101
+ .join("\n");
102
+ return `${header(setup.dir.replace(/\/?$/, "/"), "the locale files")}
103
+ //! The messages of every locale, compiled by vue-i18n's own message compiler.
104
+
105
+ use ferrovue::i18n::{Locale, Message, Part};
106
+
107
+ /// The locale a page renders in when it names none.
108
+ pub const LOCALE: &str = ${rustStr(setup.locale)};
109
+
110
+ /// The locales a missing message falls back to, in order.
111
+ pub const FALLBACK: &[&str] = &[${setup.fallback.map(rustStr).join(", ")}];
112
+
113
+ /// Every locale's messages.
114
+ pub static LOCALES: &[Locale] = &[
115
+ ${locales}
116
+ ];
117
+
118
+ /// The messages in \`locale\`, with the configured fallbacks: build one per request.
119
+ pub fn i18n(locale: &str) -> ferrovue::I18n {
120
+ ferrovue::I18n::new(LOCALES, locale, FALLBACK)
121
+ }
122
+ `;
123
+ }
124
+ /** A value interpolated into a message, as \`ferrovue::i18n::Value\`. */
125
+ function i18nValue(s, n) {
126
+ const v = expr(s, n);
127
+ switch (v.ty.k) {
128
+ case "str":
129
+ return `fv::i18n::Value::Str(${strArg(v.code)})`;
130
+ case "int":
131
+ return `fv::i18n::Value::Int(${bare(v.code)})`;
132
+ case "float":
133
+ return `fv::i18n::Value::Float(${bare(v.code)})`;
134
+ case "bool":
135
+ return `fv::i18n::Value::Bool(${bare(v.code)})`;
136
+ default:
137
+ return fail(s.comp, "a value given to `t()` is a string, a number or a boolean that is present: narrow an optional one with `v-if` first", n);
138
+ }
139
+ }
140
+ /** `t(key)`, `t(key, plural)`, `t(key, { named })`, `t(key, [list])`, `t(key, { named }, plural)`:
141
+ * the message translated in the request's locale. */
142
+ export function translate(s, args, n) {
143
+ if (!ctx.i18n)
144
+ fail(s.comp, "`t()` needs `i18n` in ferrovue.config.json: where the locale files are", n);
145
+ if (args.length < 1 || args.length > 3)
146
+ fail(s.comp, "`t()` takes a key, then named values, a list or a plural number", n);
147
+ const key = expr(s, args[0]);
148
+ if (key.ty.k !== "str")
149
+ fail(s.comp, "the key given to `t()` is a string", args[0]);
150
+ let named = "&[]";
151
+ let list = "&[]";
152
+ let plural = "None";
153
+ for (const a of args.slice(1)) {
154
+ if (a.type === "ObjectExpression") {
155
+ const pairs = a.properties.map((p) => {
156
+ if (p.type !== "ObjectProperty" || p.computed)
157
+ fail(s.comp, "named values hold plain keys", p);
158
+ return `(${rustStr(String(p.key.name ?? p.key.value))}, ${i18nValue(s, p.value)})`;
159
+ });
160
+ named = `&[${pairs.join(", ")}]`;
161
+ }
162
+ else if (a.type === "ArrayExpression") {
163
+ list = `&[${a.elements.map((el) => i18nValue(s, el)).join(", ")}]`;
164
+ }
165
+ else {
166
+ const v = expr(s, a);
167
+ if (v.ty.k === "str")
168
+ fail(s.comp, "a default message given to `t()` is not supported: add the message to the locale files", a);
169
+ if (v.ty.k !== "int")
170
+ fail(s.comp, "the plural number given to `t()` is an integer", a);
171
+ plural = `Some(${bare(v.code)})`;
172
+ }
173
+ }
174
+ return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args { named: ${named}, list: ${list}, plural: ${plural} })`, ty: STR };
175
+ }
@@ -0,0 +1,19 @@
1
+ import { type N, type Scope, type Ty, type Val } from "./model.ts";
2
+ /** A list as an iterator of its items, in the form a computed list holds them. */
3
+ export declare function items(v: Val): string;
4
+ /** A computed list: its iterator, and its items collected as `code` for anything that needs a
5
+ * `Vec`. */
6
+ export declare function computed(iter: string, of: Ty, lone?: boolean): Val;
7
+ /** A list of any kind as the `Vec` of items a computed list collects: what a branch of `?:` or a
8
+ * setup `let` holds, so that both branches have one type. Kept beyond the statement, its strings
9
+ * own their text, which may live in a temporary (`name.toUpperCase().split(" ")`). */
10
+ export declare function collected(v: Val): string;
11
+ /** The computed list a setup `let` named `name` holds, read again by item. */
12
+ export declare function heldList(name: string, of: Ty, lone?: boolean): Val;
13
+ /** A list method other than `includes` and `join`: `null` when `method` is not one of them. */
14
+ export declare function listMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
15
+ /** `Object.keys(r)`, `Object.values(r)`, and `Object.entries(r)`, which is only a `v-for`'s source. */
16
+ export declare function objectCall(s: Scope, method: string, args: N[], n: N): Val;
17
+ /** `list.includes(x)` and `list.join(sep)` of a computed list, or of a list of fractions or
18
+ * booleans. */
19
+ export declare function computedListMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
package/dist/lists.js ADDED
@@ -0,0 +1,253 @@
1
+ /* Lists a template computes: array methods taking arrow functions (`filter`, `map`, `some`,
2
+ * `every`, `find`, `findIndex`), `slice`, and `Object.keys` / `values` of a record.
3
+ *
4
+ * A computed list is an iterator of its items, each in one form whatever list it came from: a
5
+ * string as a `Cow<str>` (borrowed from the props, or owned when `map` made it), a number or a
6
+ * boolean copied, an object borrowed. That is what lets one method's result feed the next, a
7
+ * `v-for`, `.join` or `.length`, without knowing where it came from. */
8
+ import { BOOL, fail, FLOAT, INT, opt, sameTy, STR } from "./model.js";
9
+ import { ctx } from "./context.js";
10
+ import { asCow, yieldsCow, asF64, cond, expr, fieldVal, isNumber, isTemporary, lonely, meet } from "./expr.js";
11
+ import { atom, operand, strArg, UNARY } from "./parens.js";
12
+ const COW = "std::borrow::Cow::<str>";
13
+ /** The type of a list's items, which must be one these methods can pass along. */
14
+ function itemsTy(s, v, n) {
15
+ if (v.ty.k !== "list")
16
+ return fail(s.comp, "not a list", n);
17
+ const of = v.ty.of;
18
+ if (!["str", "int", "float", "bool", "struct", "child"].includes(of.k)) {
19
+ fail(s.comp, "this method takes a list of strings, numbers, booleans or objects", n);
20
+ }
21
+ return of;
22
+ }
23
+ /** A list as an iterator of its items, in the form a computed list holds them. */
24
+ export function items(v) {
25
+ if (v.iter !== undefined)
26
+ return v.iter;
27
+ const of = v.ty.k === "list" || v.ty.k === "record" ? v.ty.of : v.ty;
28
+ // A list of strings holds `Cow`s, an array literal `&str`s: either way `&**v` is the `&str`.
29
+ if (of.k === "str")
30
+ return `${atom(v.code)}.iter().map(|v| ${COW}::Borrowed(&**v))`;
31
+ if (of.k === "struct" || of.k === "child")
32
+ return `${atom(v.code)}.iter()`;
33
+ return `${atom(v.code)}.iter().copied()`;
34
+ }
35
+ /** A computed list: its iterator, and its items collected as `code` for anything that needs a
36
+ * `Vec`. */
37
+ export function computed(iter, of, lone) {
38
+ return { code: `${iter}.collect::<Vec<_>>()`, ty: { k: "list", of }, iter, ...(lone ? { lone } : {}) };
39
+ }
40
+ /** A list of any kind as the `Vec` of items a computed list collects: what a branch of `?:` or a
41
+ * setup `let` holds, so that both branches have one type. Kept beyond the statement, its strings
42
+ * own their text, which may live in a temporary (`name.toUpperCase().split(" ")`). */
43
+ export function collected(v) {
44
+ const of = v.ty.k === "list" ? v.ty.of : v.ty;
45
+ return `${items(v)}${of.k === "str" ? `.map(|v| ${COW}::Owned(v.into_owned()))` : ""}.collect::<Vec<_>>()`;
46
+ }
47
+ /** The computed list a setup `let` named `name` holds, read again by item. */
48
+ export function heldList(name, of, lone) {
49
+ const iter = of.k === "str" ? `${name}.iter().map(|v| ${COW}::Borrowed(&**v))` : `${name}.iter().copied()`;
50
+ return { code: name, ty: { k: "list", of }, iter, ...(lone ? { lone } : {}) };
51
+ }
52
+ /** An item as the closure that a method hands it to sees it: `byRef` for `filter` and `find`, which
53
+ * pass `&item`. */
54
+ function itemVal(of, param, byRef, lone) {
55
+ // An object's fields are read through the reference; a number or a boolean is copied out of it.
56
+ const scalar = of.k === "int" || of.k === "float" || of.k === "bool";
57
+ const code = of.k === "str" ? (byRef ? `&**${param}` : `&*${param}`) : byRef && scalar ? `*${param}` : param;
58
+ return { code, ty: of, ...(lone ? { lone } : {}) };
59
+ }
60
+ /** An arrow function's body, translated with its parameters bound — the item, and the index when it
61
+ * names one — as the closure a method takes. `enumerate` says the items must come numbered. */
62
+ function arrow(s, fn, list, of, byRef, method, body) {
63
+ const comp = s.comp;
64
+ if (fn?.type !== "ArrowFunctionExpression" || fn.async || fn.params.length > 2) {
65
+ fail(comp, `\`.${method}()\` takes an arrow function of the item, and of its index: \`x => …\`, \`(x, i) => …\``, fn);
66
+ }
67
+ if (fn.body.type === "BlockStatement")
68
+ fail(comp, `\`.${method}()\` takes an arrow function whose body is an expression, without \`{ return … }\``, fn);
69
+ const [item, index] = fn.params;
70
+ if (index && index.type !== "Identifier")
71
+ fail(comp, "an arrow function's index is a plain name", index);
72
+ const n = ++ctx.narrowCount;
73
+ // A string item is named apart: `map` cannot hand back a borrow of it (see `mapped`).
74
+ const p = of.k === "str" ? `fv_s${n}` : `fv_a${n}`;
75
+ const i = `fv_i${n}`;
76
+ const v = itemVal(of, p, byRef, list.lone);
77
+ const locals = new Map(s.locals);
78
+ const bound = [];
79
+ if (item?.type === "Identifier") {
80
+ locals.set(item.name, v);
81
+ bound.push(item.name);
82
+ }
83
+ else if (item?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
84
+ // `({ id, label }) => …`: each name is that field of the item.
85
+ for (const prop of item.properties) {
86
+ if (prop.type !== "ObjectProperty" || prop.computed || prop.value.type !== "Identifier") {
87
+ fail(comp, "a destructured arrow function parameter binds plain names, without defaults", prop);
88
+ }
89
+ locals.set(prop.value.name, fieldVal(comp, v.code, of, prop.key.name ?? prop.key.value, prop));
90
+ bound.push(prop.value.name);
91
+ }
92
+ }
93
+ else if (item)
94
+ fail(comp, "an arrow function's item is a name, or an object pattern of an object", item);
95
+ if (index) {
96
+ locals.set(index.name, { code: i, ty: INT });
97
+ bound.push(index.name);
98
+ }
99
+ // The parameters hide whatever an outer `v-if` narrowed under the same names.
100
+ const narrowed = new Map([...s.narrowed].filter(([path]) => !bound.some((b) => path === b || path.startsWith(`${b}.`))));
101
+ const code = body({ ...s, locals, narrowed });
102
+ const reads = (name) => new RegExp(`\\b${name}\\b`).test(code);
103
+ const pName = reads(p) ? p : "_";
104
+ if (!index)
105
+ return { closure: `|${pName}| ${code}`, enumerate: false };
106
+ const lets = (reads(i) ? `let ${i} = ${i} as i64; ` : "");
107
+ if (byRef) {
108
+ const e = `fv_e${n}`;
109
+ const itemLet = reads(p) ? `let ${p} = &${e}.1; ` : "";
110
+ const indexLet = reads(i) ? `let ${i} = ${e}.0 as i64; ` : "";
111
+ return { closure: `|${itemLet || indexLet ? e : "_"}| { ${itemLet}${indexLet}${code} }`, enumerate: true };
112
+ }
113
+ return { closure: `|(${reads(i) ? i : "_"}, ${pName})| { ${lets}${code} }`, enumerate: true };
114
+ }
115
+ /** What `map` makes of each item: a string as a `Cow` — owned when it was built, or borrows the
116
+ * item, which the closure owns — a number or a boolean as it is, an object borrowed. */
117
+ function mapped(s, v, n) {
118
+ switch (v.ty.k) {
119
+ case "str":
120
+ // A routine's `Cow` may borrow the item, which the closure owns: owned.
121
+ if (v.code.startsWith("&*fv::") && yieldsCow(v.code.slice(2)))
122
+ return `${COW}::Owned(${v.code.slice(2)}.into_owned())`;
123
+ // An item itself, of this list or one outside it, which the closure only borrows: copied.
124
+ const item = /^&\**(fv_s\d+)$/.exec(v.code)?.[1];
125
+ if (item !== undefined)
126
+ return `${COW}::Owned(${item}.to_string())`;
127
+ if (isTemporary(v))
128
+ return asCow(v);
129
+ return /\bfv_s\d+\b/.test(v.code) ? `${COW}::Owned(${atom(v.code)}.to_owned())` : `${COW}::Borrowed(${strArg(v.code)})`;
130
+ case "int":
131
+ case "float":
132
+ case "bool":
133
+ return v.code;
134
+ case "struct":
135
+ case "child":
136
+ return `&${operand(v.code, UNARY)}`;
137
+ default:
138
+ return fail(s.comp, `\`.map()\` makes a list of strings, numbers, booleans or objects, not ${v.ty.k === "opt" ? "optional values" : "these"}`, n);
139
+ }
140
+ }
141
+ /** A list method other than `includes` and `join`: `null` when `method` is not one of them. */
142
+ export function listMethod(s, target, method, args, n) {
143
+ const comp = s.comp;
144
+ if (!["filter", "map", "some", "every", "find", "findIndex", "slice"].includes(method))
145
+ return null;
146
+ const of = itemsTy(s, target, n);
147
+ const it = items(target);
148
+ if (method === "slice") {
149
+ if (args.length < 1 || args.length > 2)
150
+ fail(comp, "`.slice()` takes a start, and an end", n);
151
+ const [start, end] = args.map((a) => expr(s, a));
152
+ const index = (v) => {
153
+ if (v === undefined || v.ty.k === "undef")
154
+ return null;
155
+ if (!isNumber(v.ty))
156
+ fail(comp, "`.slice()` takes numbers", n);
157
+ return asF64(v);
158
+ };
159
+ const from = index(start) ?? "0.0";
160
+ const to = index(end);
161
+ return computed(`fv::js_slice_items(${it}, ${from}, ${to === null ? "None" : `Some(${to})`})`, of, target.lone);
162
+ }
163
+ if (args.length !== 1)
164
+ fail(comp, `\`.${method}()\` takes one arrow function`, n);
165
+ const fn = args[0];
166
+ switch (method) {
167
+ case "filter": {
168
+ const { closure, enumerate } = arrow(s, fn, target, of, true, method, (inner) => cond(inner, fn.body));
169
+ return computed(enumerate ? `${it}.enumerate().filter(${closure}).map(|(_, v)| v)` : `${it}.filter(${closure})`, of, target.lone);
170
+ }
171
+ case "map": {
172
+ let out;
173
+ const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => {
174
+ out = expr(inner, fn.body);
175
+ // An integer negated, multiplied or taken modulo may be JavaScript's -0, which a list of
176
+ // `i64` loses: a list of fractions then. (Generated code spaces a binary `-`, never a unary one.)
177
+ if (out.ty.k === "int" && out.f64 !== undefined && /-[\w(]|\s[*%]\s/.test(out.f64))
178
+ out = { ...out, code: out.f64, ty: FLOAT };
179
+ return mapped(inner, out, fn.body);
180
+ });
181
+ return computed(`${it}${enumerate ? ".enumerate()" : ""}.map(${closure})`, out.ty, out.lone);
182
+ }
183
+ case "some":
184
+ case "every": {
185
+ const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => cond(inner, fn.body));
186
+ return { code: `${it}${enumerate ? ".enumerate()" : ""}.${method === "some" ? "any" : "all"}(${closure})`, ty: BOOL };
187
+ }
188
+ case "find": {
189
+ const { closure, enumerate } = arrow(s, fn, target, of, true, method, (inner) => cond(inner, fn.body));
190
+ const found = enumerate ? `${it}.enumerate().find(${closure}).map(|(_, v)| v)` : `${it}.find(${closure})`;
191
+ const lone = target.lone ? { lone: true } : {};
192
+ // A string found is a `Cow` the statement holds, read as the `&str` an optional string is; one
193
+ // kept beyond the statement owns its text, which may live in a temporary.
194
+ if (of.k === "str")
195
+ return { code: `${atom(found)}.as_deref()`, ty: opt(STR), held: `${found}.map(|v| ${COW}::Owned(v.into_owned()))`, ...lone };
196
+ return { code: found, ty: opt(of), ...lone };
197
+ }
198
+ default: {
199
+ const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => cond(inner, fn.body));
200
+ return { code: `${it}${enumerate ? ".enumerate()" : ""}.position(${closure}).map_or(-1, |i| i as i64)`, ty: INT };
201
+ }
202
+ }
203
+ }
204
+ /** `Object.keys(r)`, `Object.values(r)`, and `Object.entries(r)`, which is only a `v-for`'s source. */
205
+ export function objectCall(s, method, args, n) {
206
+ const comp = s.comp;
207
+ const r = args.length === 1 ? expr(s, args[0]) : null;
208
+ if (r?.ty.k !== "record")
209
+ return fail(comp, `\`Object.${method}()\` takes a \`Record<string, T>\``, n);
210
+ const of = r.ty.of;
211
+ if (method === "keys")
212
+ return computed(`${atom(r.code)}.keys().map(${COW}::Borrowed)`, STR);
213
+ if (method === "values") {
214
+ if (of.k === "list")
215
+ fail(comp, "`Object.values()` of a record of lists, which these methods cannot walk", n);
216
+ const values = of.k === "str" ? `.values().map(|v| ${COW}::Borrowed(&**v))` : of.k === "struct" || of.k === "child" ? ".values()" : ".values().copied()";
217
+ return computed(`${atom(r.code)}${values}`, of);
218
+ }
219
+ if (method === "entries")
220
+ return fail(comp, "`Object.entries()` is supported as the source of a `v-for`: `([key, value], i) in Object.entries(r)`", n);
221
+ return fail(comp, `\`Object.${method}()\` is not supported: \`keys\`, \`values\` and \`entries\` of a record are`, n);
222
+ }
223
+ /** `list.includes(x)` and `list.join(sep)` of a computed list, or of a list of fractions or
224
+ * booleans. */
225
+ export function computedListMethod(s, target, method, args, n) {
226
+ if (target.ty.k !== "list")
227
+ return null;
228
+ const of = target.ty.of;
229
+ if (method === "includes" && args.length === 1) {
230
+ const x = expr(s, args[0]);
231
+ if (!sameTy(x.ty, of) || !["str", "int", "float", "bool"].includes(of.k))
232
+ fail(s.comp, "`.includes()` looks for a value of the list's own type", args[0]);
233
+ if (of.k === "str")
234
+ meet(s.comp, target, x, "`.includes()`", n, "equal");
235
+ // `includes` finds `NaN`, which `==` never equals.
236
+ const test = of.k === "str" ? `*v == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}` : of.k === "float" ? `v == ${x.code} || (v.is_nan() && ${atom(x.code)}.is_nan())` : `v == ${x.code}`;
237
+ return { code: `${items(target)}.any(|v| ${test})`, ty: BOOL };
238
+ }
239
+ if (method === "join" && args.length <= 1) {
240
+ if (!["str", "int", "float", "bool"].includes(of.k))
241
+ return null;
242
+ const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
243
+ if (sep.ty.k !== "str")
244
+ fail(s.comp, "`.join()` takes a string", args[0]);
245
+ // Two halves of a pair would meet where items are joined without a separator between them.
246
+ const parted = /^"[^"]/.test(sep.code);
247
+ if (of.k === "str" && target.lone && (!parted || sep.lone))
248
+ fail(s.comp, lonely("`.join()` with no literal separator"), n);
249
+ const each = of.k === "str" ? "" : of.k === "bool" ? `.map(|v| if v { "true" } else { "false" })` : ".map(|v| fv::Js(v).to_string())";
250
+ return { code: `&*${items(target)}${each}.collect::<Vec<_>>().join(${strArg(sep.code)})`, ty: STR, ...(target.lone || sep.lone ? { lone: true } : {}) };
251
+ }
252
+ return null;
253
+ }