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,248 @@
1
+ import type { SourceMapConsumer } from "source-map-js";
2
+ export type N = any;
3
+ export type Ty = {
4
+ k: "str";
5
+ } | {
6
+ k: "int";
7
+ }
8
+ /** `Float` from `ferrovue/types`, or a fractional literal: an `f64`, a JavaScript number. */
9
+ | {
10
+ k: "float";
11
+ } | {
12
+ k: "bool";
13
+ } | {
14
+ k: "undef";
15
+ } | {
16
+ k: "opt";
17
+ of: Ty;
18
+ } | {
19
+ k: "list";
20
+ of: Ty;
21
+ }
22
+ /** `Record<string, T>`: an object used as a dictionary, `ferrovue::Record` in Rust, its keys in
23
+ * JavaScript's order. */
24
+ | {
25
+ k: "record";
26
+ of: Ty;
27
+ }
28
+ /** `store` marks a Pinia store's state, or a type inside it, declared in a store's own file;
29
+ * `home` a type declared elsewhere: `"types"` for a shared `.ts` file, or the component whose
30
+ * `.vue` file declares it. */
31
+ | {
32
+ k: "struct";
33
+ name: string;
34
+ store?: true;
35
+ home?: string;
36
+ }
37
+ /** `TrustedHtml` from `ferrovue/types`: the configured Rust type, which `v-html` writes raw. */
38
+ | {
39
+ k: "html";
40
+ }
41
+ /** Another component's `Props`, imported from its `.vue` file: what `v-bind` hands that child. */
42
+ | {
43
+ k: "child";
44
+ name: string;
45
+ }
46
+ /** `useRoute()` or `$route`: the reader's location, as vue-router resolved it. */
47
+ | {
48
+ k: "route";
49
+ }
50
+ /** `route.params`. */
51
+ | {
52
+ k: "params";
53
+ }
54
+ /** `route.query`, and one of its values: a string, \`null\`, an array of those, or absent. */
55
+ | {
56
+ k: "queryobj";
57
+ } | {
58
+ k: "query";
59
+ };
60
+ export interface Field {
61
+ js: string;
62
+ rust: string;
63
+ ty: Ty;
64
+ /** For an optional prop: the Rust value Vue uses when it is absent — its declared default, or
65
+ * `false` for a boolean, which Vue casts. Reading the prop reads this in place of `None`. */
66
+ dflt?: string;
67
+ }
68
+ export interface Struct {
69
+ name: string;
70
+ fields: Field[];
71
+ /** The props a scoped slot's outlet passes: fields that borrow from the render, read as they are. */
72
+ slot?: true;
73
+ }
74
+ /** A translated expression: Rust source, and the type it evaluates to. `konst` is a boolean known
75
+ * at generation time, which is how `Array.isArray` on a value that is never an array folds away. */
76
+ export interface Val {
77
+ code: string;
78
+ ty: Ty;
79
+ konst?: boolean;
80
+ /** For an integer computed from others: the same computation on doubles, before it is rounded
81
+ * back to an \`i64\` — which keeps JavaScript's \`-0\`, so a division by it is \`-Infinity\`. */
82
+ f64?: string;
83
+ /** For a list a template computes — `filter`, `map`, `split`, `Object.keys` — an iterator of its
84
+ * items, each a `Cow<str>`, a copied number or boolean, or a borrowed object. `code` is then those
85
+ * items collected into a `Vec`. */
86
+ iter?: string;
87
+ /** For a string, or a list of strings: it may hold half of a surrogate pair, which JavaScript
88
+ * keeps and ferrovue writes as U+FFFD — so two such values are never compared or joined. */
89
+ lone?: boolean;
90
+ /** For an optional string a temporary owns: that `Option<Cow<str>>`, whose `.as_deref()` is
91
+ * `code`. A setup `let` keeps this. */
92
+ held?: string;
93
+ /** A number known at generation time: a literal, or literals JavaScript computed. */
94
+ num?: number;
95
+ /** For a string built by `format!`: its format string and arguments, which a string built from it
96
+ * joins rather than formatting it again. */
97
+ format?: {
98
+ text: string;
99
+ args: string[];
100
+ };
101
+ }
102
+ export declare const STR: Ty;
103
+ export declare const INT: Ty;
104
+ export declare const FLOAT: Ty;
105
+ export declare const BOOL: Ty;
106
+ export declare const UNDEF: Ty;
107
+ export declare const opt: (of: Ty) => Ty;
108
+ export declare const RUST_KEYWORDS: Set<string>;
109
+ /** Types the generated code names, which an interface of the same name would shadow. */
110
+ export declare const RUST_PRELUDE: Set<string>;
111
+ export declare function snake(js: string): string;
112
+ export declare function rustStr(s: string): string;
113
+ /** One character as a Rust `char` literal. */
114
+ export declare function rustChar(ch: string): string;
115
+ export declare class GenError extends Error {
116
+ }
117
+ export interface Component {
118
+ /** `TagEditor` */
119
+ name: string;
120
+ /** `tag_editor` */
121
+ module: string;
122
+ file: string;
123
+ /** The file's text, which an error quotes. */
124
+ source?: string;
125
+ /** Vue's compiled template mapped back to the template, and where the template's content starts
126
+ * in the file, for an error found in the compiled code. */
127
+ templateMap?: SourceMapConsumer;
128
+ templateStart?: {
129
+ line: number;
130
+ column: number;
131
+ };
132
+ props: Struct;
133
+ structs: Map<string, Struct>;
134
+ /** The local name `TrustedHtml` was imported under from `ferrovue/types`, if it was. */
135
+ trustedName: string | null;
136
+ /** The local name `Float` was imported under from `ferrovue/types`, if it was. */
137
+ floatName: string | null;
138
+ /** Types imported as another component's `Props`: local name → that component's name. */
139
+ childProps: Map<string, string>;
140
+ /** The components it imports, by name. */
141
+ imports: Set<string>;
142
+ /** The slots its template renders with `<slot>`, by name, in order of first appearance. */
143
+ slotNames: string[];
144
+ /** Whether its template holds `<RouterView>`: the page, which the server supplies. */
145
+ routerView: boolean;
146
+ /** Whether its template holds `<RouterLink>`. */
147
+ routerLink: boolean;
148
+ /** Whether it reads the route itself: `useRoute()`, or `$route` in the template. */
149
+ readsRoute: boolean;
150
+ /** Whether it renders a `<RouterLink>` or reads the route, itself or through a child, and so
151
+ * takes the route. */
152
+ usesRoute: boolean;
153
+ /** Whether its setup reads a store. */
154
+ readsStores: boolean;
155
+ /** Whether it reads a store, itself or through a child, and so takes the stores' state. */
156
+ usesStores: boolean;
157
+ /** Whether it translates — \`$t\`, or \`useI18n()\` in setup — and whether it or a child does, and
158
+ * so takes the request's \`I18n\`. */
159
+ readsI18n: boolean;
160
+ usesI18n: boolean;
161
+ /** Whether it renders a \`<Teleport>\`, and whether it or a child does, and so takes the page's
162
+ * \`Teleports\`. */
163
+ readsTeleports: boolean;
164
+ usesTeleports: boolean;
165
+ /** `defineModel` bindings: local name → the prop it reads. */
166
+ models: Map<string, string>;
167
+ /** Type aliases it declares: name → the type. */
168
+ aliases: Map<string, N>;
169
+ /** Types it imports from elsewhere: local name → the type, marked with where it lives. */
170
+ importedTypes: Map<string, Ty>;
171
+ /** Its scoped slots, by name: the props each one's outlets pass, known once its render is
172
+ * generated, which is why a child is generated before its parents. */
173
+ slotShapes: Map<string, Struct>;
174
+ /** `data-v-…`, the id its `<style scoped>` gives its elements, or `null` without one. */
175
+ scopeId: string | null;
176
+ /** Whether its scoped styles use `:slotted()`, so that its outlets pass a slot scope id. */
177
+ slotted: boolean;
178
+ /** `inheritAttrs` from `defineOptions`: `false` drops what a parent passes on to its root. */
179
+ inheritAttrs: boolean;
180
+ /** Whether a parent may hand its root scope ids — its own, those passed on to it, or a slot's —
181
+ * which it then takes as `fv_attrs`. Known once every component is read (`scopeFlow`). */
182
+ inherits: boolean;
183
+ /** Whether its outlets may pass slot content a slot scope id, which the content then takes as
184
+ * `fv_sid`. Known once every component is read, as `inherits` is. */
185
+ passesSlotIds: boolean;
186
+ }
187
+ export interface Scope {
188
+ comp: Component;
189
+ components: Map<string, Component>;
190
+ /** Setup bindings the server can evaluate: `ref(...)` and `computed(() => ...)`. */
191
+ setup: Map<string, Val>;
192
+ /** Imported child components, by local name. */
193
+ children: Map<string, string>;
194
+ /** Imported helpers, by local name → helper name. */
195
+ helpers: Map<string, string>;
196
+ /** Loop variables in scope. */
197
+ locals: Map<string, Val>;
198
+ /** The identifier `defineProps` was assigned to in the script, if any. */
199
+ propsIdent: string | null;
200
+ /** Setup bindings that are refs — `ref`, `computed`, `storeToRefs`, `defineModel` — which code in
201
+ * the script reads through `.value`, and the template unwrapped. */
202
+ refs: Set<string>;
203
+ /** Setup bindings the server does not evaluate, and why. The template may name one only from an
204
+ * event handler, which the server drops. */
205
+ clientOnly: Map<string, string>;
206
+ /** Optional values a surrounding `v-if` has found present, by the JavaScript path that named them
207
+ * — what TypeScript narrows, and what the generated `if let` bound. */
208
+ narrowed: Map<string, Val>;
209
+ /** The name the compiled template resolved this component itself under, when it uses itself. */
210
+ selfAlias: {
211
+ name: string | null;
212
+ };
213
+ /** What the helper calls translated so far can write, shared by every copy of the scope. */
214
+ helperBytes: {
215
+ n: number;
216
+ };
217
+ /** The names the compiled template resolved `RouterLink` and `RouterView` under. */
218
+ router: Map<string, "RouterLink" | "RouterView">;
219
+ /** Directives the compiled template resolved by name: local → the directive's name. */
220
+ directives: Map<string, string>;
221
+ /** Setup bindings that are vue-i18n's \`t\`, from \`const { t } = useI18n()\`. */
222
+ i18nT: Set<string>;
223
+ /** Inside slot content whose emptiness is decided at run time: each push that is not a comment
224
+ * sets the closure's `filled`, which is how Vue tells content from nothing (`ssrRenderSlot`). */
225
+ fill: boolean;
226
+ /** Inside a `<RouterLink>`'s slot, which Vue renders from virtual nodes rather than pushes: an
227
+ * untaken `v-if` is `<!--v-if-->` there, not `<!---->`. */
228
+ vnode: boolean;
229
+ /** The Rust value of `_attrs`, the scope ids this render's root inherits, as `ssrRenderAttrs`
230
+ * writes them; `null` when the component inherits none. */
231
+ attrs: string | null;
232
+ /** The Rust value of `_scopeId` inside slot content: the slot scope id the content is given, or
233
+ * `null` where it is always empty. */
234
+ sid: string | null;
235
+ /** Inside a `v-for` over a list of the props: the Rust name of its item, and the list. */
236
+ loop?: {
237
+ item: string;
238
+ over: string;
239
+ };
240
+ }
241
+ /** Where a node came from, which decides how its position is read: `source` for an AST parsed
242
+ * from the file itself (a script block, a store, a type file), whose positions are the file's;
243
+ * `template` for Vue's compiled template, whose positions map back through its source map. */
244
+ export type Origin = "source" | "template";
245
+ /** Mark every node of an AST with where it came from, so that an error can point into the file. */
246
+ export declare function tagAst(node: N, origin: Origin, seen?: WeakSet<object>): void;
247
+ export declare function fail(comp: Component, what: string, node?: N): never;
248
+ export declare function sameTy(a: Ty, b: Ty): boolean;
package/dist/model.js ADDED
@@ -0,0 +1,102 @@
1
+ /* The compiler's model: the types it gives values, the shapes of components and scopes, and the errors it raises. (Not `types.ts`, which is the public `ferrovue/types` module.) */
2
+ export const STR = { k: "str" };
3
+ export const INT = { k: "int" };
4
+ export const FLOAT = { k: "float" };
5
+ export const BOOL = { k: "bool" };
6
+ export const UNDEF = { k: "undef" };
7
+ export const opt = (of) => (of.k === "opt" ? of : { k: "opt", of });
8
+ export const RUST_KEYWORDS = new Set("as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn abstract become box do final macro override priv typeof unsized virtual yield try".split(" "));
9
+ /** Types the generated code names, which an interface of the same name would shadow. */
10
+ export const RUST_PRELUDE = new Set(["Option", "Some", "None", "Vec", "String", "Box", "Result", "Ok", "Err", "Cow", "Slots", "Default"]);
11
+ export function snake(js) {
12
+ const s = js.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase()).replace(/^_/, "");
13
+ return RUST_KEYWORDS.has(s) ? `r#${s}` : s;
14
+ }
15
+ export function rustStr(s) {
16
+ let out = '"';
17
+ for (const ch of s) {
18
+ const c = ch.codePointAt(0);
19
+ if (ch === '"')
20
+ out += '\\"';
21
+ else if (ch === "\\")
22
+ out += "\\\\";
23
+ else if (ch === "\n")
24
+ out += "\\n";
25
+ else if (ch === "\r")
26
+ out += "\\r";
27
+ else if (ch === "\t")
28
+ out += "\\t";
29
+ else if (c < 0x20 || c === 0x7f)
30
+ out += `\\u{${c.toString(16)}}`;
31
+ else
32
+ out += ch;
33
+ }
34
+ return out + '"';
35
+ }
36
+ /** One character as a Rust `char` literal. */
37
+ export function rustChar(ch) {
38
+ if (ch === "'")
39
+ return "'\\''";
40
+ if (ch === '"')
41
+ return `'"'`;
42
+ return `'${rustStr(ch).slice(1, -1)}'`;
43
+ }
44
+ export class GenError extends Error {
45
+ }
46
+ /** Mark every node of an AST with where it came from, so that an error can point into the file. */
47
+ export function tagAst(node, origin, seen = new WeakSet()) {
48
+ // `compileScript` links some nodes back to others, so the tree may hold cycles.
49
+ if (!node || typeof node !== "object" || seen.has(node))
50
+ return;
51
+ seen.add(node);
52
+ if (Array.isArray(node)) {
53
+ for (const n of node)
54
+ tagAst(n, origin, seen);
55
+ return;
56
+ }
57
+ if (typeof node.type === "string")
58
+ node.__fv = origin;
59
+ for (const key of Object.keys(node)) {
60
+ if (key === "loc" || key === "__fv")
61
+ continue;
62
+ const v = node[key];
63
+ if (v && typeof v === "object")
64
+ tagAst(v, origin, seen);
65
+ }
66
+ }
67
+ /** A node's position in the component's own file — line from 1, column from 0 — or `null`. */
68
+ function locate(comp, node) {
69
+ const start = node?.loc?.start;
70
+ if (!start)
71
+ return null;
72
+ if (node.__fv === "source")
73
+ return { line: start.line, column: start.column };
74
+ if (node.__fv === "template" && comp.templateMap && comp.templateStart) {
75
+ const o = comp.templateMap.originalPositionFor({ line: start.line, column: start.column });
76
+ if (o.line == null || o.column == null)
77
+ return null;
78
+ // The template's content starts partway along the `<template>` line.
79
+ const t = comp.templateStart;
80
+ return o.line === 1 ? { line: t.line, column: t.column - 1 + o.column } : { line: t.line + o.line - 1, column: o.column };
81
+ }
82
+ return null;
83
+ }
84
+ /** The source line, with a caret under the column, as rustc shows it. */
85
+ function snippet(comp, at) {
86
+ const text = comp.source?.split("\n")[at.line - 1];
87
+ if (text === undefined)
88
+ return "";
89
+ const n = String(at.line);
90
+ // Tabs are kept, so the caret lines up whatever the tab width.
91
+ const lead = text.slice(0, at.column).replace(/[^\t]/g, " ");
92
+ return `\n ${n} | ${text}\n ${" ".repeat(n.length)} | ${lead}^`;
93
+ }
94
+ export function fail(comp, what, node) {
95
+ const at = locate(comp, node);
96
+ if (!at)
97
+ throw new GenError(`${comp.file}: ${what}`);
98
+ throw new GenError(`${comp.file}:${at.line}:${at.column + 1}: ${what}${snippet(comp, at)}`);
99
+ }
100
+ export function sameTy(a, b) {
101
+ return JSON.stringify(a) === JSON.stringify(b);
102
+ }
@@ -0,0 +1,51 @@
1
+ export declare const OR = 1;
2
+ export declare const CMP = 3;
3
+ export declare const AS = 6;
4
+ export declare const UNARY = 7;
5
+ /** How tightly `code` binds as an operand. Read from its text: only its outermost level counts, and
6
+ * anything the scan does not recognise counts as `LOOSE`, which only ever adds parentheses. */
7
+ export declare function binding(code: string): number;
8
+ /** How many times `code` names the variable `name`: not inside a literal, and not as a field, which
9
+ * `.name` is — though `1..=name` is the variable. */
10
+ export declare function occurrences(code: string, name: string): number;
11
+ /** `code` as an operand that must bind at least as tightly as `min`. */
12
+ export declare function operand(code: string, min: number): string;
13
+ /** `code` before `.method()` or `.field`. */
14
+ export declare function atom(code: string): string;
15
+ /** `code` as a whole expression — an argument, a binding's value, a block's result — without
16
+ * parentheses that only wrap all of it. */
17
+ export declare function bare(code: string): string;
18
+ /** Whether `code` is one bracketed group: `(…)`, `[…]`, `{…}`. */
19
+ export declare function enclosed(code: string): boolean;
20
+ /** `code` as an `if`'s condition: bare, unless it is a block, whose `{` Rust would read as the
21
+ * start of the body. */
22
+ export declare function condition(code: string): string;
23
+ /** `a op b`, each side parenthesised only where Rust would read it otherwise. Rust's comparisons do
24
+ * not chain, so both of their operands bind more tightly than they do. A float literal beside a
25
+ * value drops its suffix, the value giving it its type, but keeps it beside another literal. */
26
+ export declare function binary(a: string, op: string, b: string): string;
27
+ /** `-code`, where `-(-x)` is `x`. */
28
+ export declare function negate(code: string): string;
29
+ /** Each ordering comparison's opposite, where neither side is NaN. */
30
+ export declare const FLIPPED: Record<string, string>;
31
+ /** `!code`, folded where it can be: `!true` is `false`, `!!x` is `x`, `!(a == b)` is `a != b`, and
32
+ * `!(a < b)` is `a >= b` unless one is NaN. */
33
+ export declare function not(code: string): string;
34
+ /** `a && b` and `a || b`, with a constant side folded away. Neither side has effects. */
35
+ export declare function logical(a: string, op: "&&" | "||", b: string): string;
36
+ /** `if test { yes } else { no }` taken apart, or `null` for anything else. */
37
+ export declare function ifElse(code: string): {
38
+ test: string;
39
+ yes: string;
40
+ no: string;
41
+ } | null;
42
+ /** A `&str` expression where a `&str` parameter takes it: `&*x` is `&x`, which deref coercion turns
43
+ * into the `&str`, and `&**v` is `v`. */
44
+ export declare function strArg(code: string): string;
45
+ /** `recv.call` for a method of `str` that returns a `bool`: on each branch when `recv` is an `if`,
46
+ * whose branches may borrow different types that only a parameter would coerce. */
47
+ export declare function callOn(recv: string, call: string): string;
48
+ /** The receiver of a method of `str` or `f64`: `&*x` and `*v` are `x` and `v`, which auto-deref
49
+ * reaches the value through. Not for a method that references have too, such as `to_owned`. A
50
+ * float literal keeps its suffix, without which the method's receiver type would be ambiguous. */
51
+ export declare function receiver(code: string): string;
package/dist/parens.js ADDED
@@ -0,0 +1,239 @@
1
+ /* Rust expressions put together with parentheses only where Rust needs them, so that generated code
2
+ * reads as written by hand and passes rustc's `unused_parens` and clippy without an allow. */
3
+ /** How tightly an expression's outermost operator binds, loosest first. `LOOSE` is anything that is
4
+ * not an operand on its own: `if … { } else { }`, a block, a closure, a range, a struct literal. */
5
+ const LOOSE = 0;
6
+ export const OR = 1;
7
+ const AND = 2;
8
+ export const CMP = 3;
9
+ const ADD = 4;
10
+ const MUL = 5;
11
+ export const AS = 6;
12
+ export const UNARY = 7;
13
+ /** A path, a literal, a call, a macro, a method chain, a field, or something already in brackets. */
14
+ const ATOM = 8;
15
+ /** Binary operators as generated code writes them, one space either side. */
16
+ const BINARY = [
17
+ [/^ \|\| /, OR],
18
+ [/^ && /, AND],
19
+ [/^ (?:==|!=|<=|>=|<|>) /, CMP],
20
+ [/^ [+-] /, ADD],
21
+ [/^ [*/%] /, MUL],
22
+ [/^ as /, AS],
23
+ ];
24
+ /** The index of the last character of a string or character literal that opens at `i`, or `i`
25
+ * itself for anything else — a lifetime's quote among them, which is never closed. */
26
+ function literalEnd(code, i) {
27
+ if (code[i] === "'") {
28
+ if (code[i + 1] === "\\")
29
+ return code.indexOf("'", i + 3);
30
+ return code[i + 2] === "'" ? i + 2 : i;
31
+ }
32
+ if (code[i] !== '"')
33
+ return i;
34
+ for (let j = i + 1; j < code.length; j++) {
35
+ if (code[j] === "\\")
36
+ j++;
37
+ else if (code[j] === '"')
38
+ return j;
39
+ }
40
+ return code.length;
41
+ }
42
+ /** The index of the bracket that closes the one at `i`, or -1. Literals are skipped. */
43
+ function closing(code, i) {
44
+ let depth = 0;
45
+ for (let j = i; j < code.length; j++) {
46
+ const ch = code[j];
47
+ if (ch === '"' || ch === "'")
48
+ j = literalEnd(code, j);
49
+ else if (ch === "(" || ch === "[" || ch === "{")
50
+ depth++;
51
+ else if (ch === ")" || ch === "]" || ch === "}") {
52
+ depth--;
53
+ if (depth === 0)
54
+ return j;
55
+ }
56
+ }
57
+ return -1;
58
+ }
59
+ /** The binary operators at the outermost level of `code`, each with where its spaced form starts and
60
+ * how tightly it binds; `null` for anything the scan does not recognise as operands and operators. */
61
+ function topLevel(code) {
62
+ if (/^(?:if|match|let|move|loop|unsafe)\b/.test(code) || code.startsWith("|") || code.startsWith("{"))
63
+ return null;
64
+ const ops = [];
65
+ for (let i = 0; i < code.length; i++) {
66
+ const ch = code[i];
67
+ if (ch === '"' || ch === "'") {
68
+ i = literalEnd(code, i);
69
+ }
70
+ else if (ch === "(" || ch === "[" || ch === "{") {
71
+ i = closing(code, i);
72
+ if (i < 0)
73
+ return null;
74
+ }
75
+ else if (ch === " ") {
76
+ const rest = code.slice(i);
77
+ const found = BINARY.find(([re]) => re.test(rest));
78
+ if (!found)
79
+ return null;
80
+ const spaced = rest.match(found[0])[0];
81
+ ops.push({ at: i, op: spaced.trim(), p: found[1] });
82
+ i += spaced.length - 1;
83
+ }
84
+ else if (ch === "." && code[i + 1] === ".") {
85
+ return null;
86
+ }
87
+ }
88
+ return ops;
89
+ }
90
+ /** How tightly `code` binds as an operand. Read from its text: only its outermost level counts, and
91
+ * anything the scan does not recognise counts as `LOOSE`, which only ever adds parentheses. */
92
+ export function binding(code) {
93
+ const ops = topLevel(code);
94
+ if (!ops)
95
+ return LOOSE;
96
+ if (ops.length)
97
+ return Math.min(...ops.map((o) => o.p));
98
+ return /^[&*!-]/.test(code) ? UNARY : ATOM;
99
+ }
100
+ /** How many times `code` names the variable `name`: not inside a literal, and not as a field, which
101
+ * `.name` is — though `1..=name` is the variable. */
102
+ export function occurrences(code, name) {
103
+ const text = code.replace(/'(?:[^'\\]|\\u\{[0-9a-f]+\}|\\.)'|"(?:[^"\\]|\\.)*"/g, '""');
104
+ return text.match(new RegExp(`(?<![\\w$])(?<![^.]\\.)${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g"))?.length ?? 0;
105
+ }
106
+ /** `code` as an operand that must bind at least as tightly as `min`. */
107
+ export function operand(code, min) {
108
+ return binding(code) >= min ? code : `(${code})`;
109
+ }
110
+ /** `code` before `.method()` or `.field`. */
111
+ export function atom(code) {
112
+ return operand(code, ATOM);
113
+ }
114
+ /** `code` as a whole expression — an argument, a binding's value, a block's result — without
115
+ * parentheses that only wrap all of it. */
116
+ export function bare(code) {
117
+ while (code.startsWith("(") && enclosed(code))
118
+ code = code.slice(1, -1);
119
+ return code;
120
+ }
121
+ /** Whether `code` is one bracketed group: `(…)`, `[…]`, `{…}`. */
122
+ export function enclosed(code) {
123
+ return /^[([{]/.test(code) && closing(code, 0) === code.length - 1;
124
+ }
125
+ /** `code` as an `if`'s condition: bare, unless it is a block, whose `{` Rust would read as the
126
+ * start of the body. */
127
+ export function condition(code) {
128
+ const inner = bare(code);
129
+ return inner.startsWith("{") ? `(${inner})` : inner;
130
+ }
131
+ /** A float literal as generated code writes one: `0.5f64`, `2.0f64`, `1e-7f64`. */
132
+ const FLOAT_LITERAL = /^-?\d[\d.]*(?:e-?\d+)?f64$/;
133
+ /** `a op b`, each side parenthesised only where Rust would read it otherwise. Rust's comparisons do
134
+ * not chain, so both of their operands bind more tightly than they do. A float literal beside a
135
+ * value drops its suffix, the value giving it its type, but keeps it beside another literal. */
136
+ export function binary(a, op, b) {
137
+ const p = BINARY.find(([re]) => re.test(` ${op} `))[1];
138
+ const plain = (x, other) => (FLOAT_LITERAL.test(x) && !FLOAT_LITERAL.test(other) ? x.slice(0, -3) : x);
139
+ // `x as f64 < y` would read the `<` as the start of the type's generic arguments.
140
+ const left = operand(plain(a, b), p === CMP ? p + 1 : p);
141
+ const cast = op === "<" && topLevel(left)?.at(-1)?.op === "as";
142
+ const right = operand(plain(b, a), p + 1);
143
+ // `&x == &y` compares what they borrow, which `x == y` does without the borrows.
144
+ if ((op === "==" || op === "!=") && /^&[^&]/.test(left) && /^&[^&]/.test(right))
145
+ return `${left.slice(1)} ${op} ${right.slice(1)}`;
146
+ return `${cast ? `(${left})` : left} ${op} ${right}`;
147
+ }
148
+ /** `-code`, where `-(-x)` is `x`. */
149
+ export function negate(code) {
150
+ if (code.startsWith("-") && binding(code) === UNARY)
151
+ return code.slice(1);
152
+ return `-${operand(code, UNARY)}`;
153
+ }
154
+ /** Each ordering comparison's opposite, where neither side is NaN. */
155
+ export const FLIPPED = { "<": ">=", ">": "<=", "<=": ">", ">=": "<" };
156
+ /** `!code`, folded where it can be: `!true` is `false`, `!!x` is `x`, `!(a == b)` is `a != b`, and
157
+ * `!(a < b)` is `a >= b` unless one is NaN. */
158
+ export function not(code) {
159
+ if (code === "true" || code === "false")
160
+ return String(code === "false");
161
+ if (code.startsWith("!") && binding(code.slice(1)) >= UNARY)
162
+ return code.slice(1);
163
+ const option = /^(.*)\.is_(some|none)\(\)$/s.exec(code);
164
+ if (option && binding(code) === ATOM)
165
+ return `${option[1]}.is_${option[2] === "some" ? "none" : "some"}()`;
166
+ const cmps = binding(code) === CMP ? topLevel(code).filter((o) => o.p === CMP) : [];
167
+ const eq = cmps.length === 1 && (cmps[0].op === "==" || cmps[0].op === "!=") ? cmps[0] : null;
168
+ if (eq)
169
+ return `${code.slice(0, eq.at)} ${eq.op === "==" ? "!=" : "=="} ${code.slice(eq.at + eq.op.length + 2)}`;
170
+ // Of doubles, `!(a < b)` also holds when either is NaN.
171
+ const order = cmps.length === 1 ? cmps[0] : null;
172
+ if (order) {
173
+ const [a, b] = [code.slice(0, order.at), code.slice(order.at + order.op.length + 2)];
174
+ const nan = [a, b].filter((x) => !/^-?[\d.]+(?:e-?\d+)?(?:f64)?$/.test(x)).map((x) => `${receiver(x)}.is_nan()`);
175
+ return [binary(a, FLIPPED[order.op], b), ...nan].join(" || ");
176
+ }
177
+ return `!${operand(code, UNARY)}`;
178
+ }
179
+ /** `a && b` and `a || b`, with a constant side folded away. Neither side has effects. */
180
+ export function logical(a, op, b) {
181
+ const absorbing = op === "&&" ? "false" : "true";
182
+ if (a === absorbing || b === absorbing)
183
+ return absorbing;
184
+ const identity = op === "&&" ? "true" : "false";
185
+ if (a === identity)
186
+ return b;
187
+ if (b === identity)
188
+ return a;
189
+ return binary(a, op, b);
190
+ }
191
+ /** `if test { yes } else { no }` taken apart, or `null` for anything else. */
192
+ export function ifElse(code) {
193
+ if (!code.startsWith("if "))
194
+ return null;
195
+ // The body opens at the first brace outside brackets: a condition holds none of its own.
196
+ let open = -1;
197
+ for (let i = 3; i < code.length && open < 0; i++) {
198
+ const ch = code[i];
199
+ if (ch === '"' || ch === "'")
200
+ i = literalEnd(code, i);
201
+ else if (ch === "(" || ch === "[")
202
+ i = closing(code, i);
203
+ else if (ch === "{")
204
+ open = i;
205
+ if (i < 0)
206
+ return null;
207
+ }
208
+ const close = open < 0 ? -1 : closing(code, open);
209
+ if (close < 0 || !code.startsWith(" else {", close + 1) || closing(code, close + 7) !== code.length - 1)
210
+ return null;
211
+ return { test: code.slice(3, open - 1), yes: code.slice(open + 2, close - 1), no: code.slice(close + 9, -2) };
212
+ }
213
+ /** A `&str` expression where a `&str` parameter takes it: `&*x` is `&x`, which deref coercion turns
214
+ * into the `&str`, and `&**v` is `v`. */
215
+ export function strArg(code) {
216
+ // Each branch of an `if` is coerced on its own.
217
+ const branches = ifElse(code);
218
+ if (branches)
219
+ return `if ${branches.test} { ${strArg(branches.yes)} } else { ${strArg(branches.no)} }`;
220
+ const m = /^&\*(\**)(.*)$/s.exec(code);
221
+ if (!m || (binding(m[2]) < ATOM && !enclosed(m[2])))
222
+ return code;
223
+ return m[1] ? m[2] : `&${m[2]}`;
224
+ }
225
+ /** `recv.call` for a method of `str` that returns a `bool`: on each branch when `recv` is an `if`,
226
+ * whose branches may borrow different types that only a parameter would coerce. */
227
+ export function callOn(recv, call) {
228
+ const branches = ifElse(recv);
229
+ if (branches)
230
+ return `if ${branches.test} { ${callOn(branches.yes, call)} } else { ${callOn(branches.no, call)} }`;
231
+ return `${receiver(recv)}.${call}`;
232
+ }
233
+ /** The receiver of a method of `str` or `f64`: `&*x` and `*v` are `x` and `v`, which auto-deref
234
+ * reaches the value through. Not for a method that references have too, such as `to_owned`. A
235
+ * float literal keeps its suffix, without which the method's receiver type would be ambiguous. */
236
+ export function receiver(code) {
237
+ const m = /^[&*]+(.*)$/s.exec(code);
238
+ return m && binding(m[1]) === ATOM ? m[1] : atom(code);
239
+ }
@@ -0,0 +1,17 @@
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
+ import { type RouteDef } from "./context.ts";
3
+ import { Emitter } from "./emitter.ts";
4
+ export declare const ROUTER_LINK_PROPS: Set<string>;
5
+ /** `<RouterLink>` props that change only what a click does, which the server does not render. */
6
+ export declare const ROUTER_LINK_INERT: Set<string>;
7
+ /** The parameter names of a route path, in order. */
8
+ export declare function routeParams(path: string): string[];
9
+ /** A value written into a URL — a parameter or a query value — as the `&str` vue-router stringifies. */
10
+ export declare function urlText(s: Scope, v: Val, n: N): string;
11
+ /** \`let fv_link = …;\`: a \`<RouterLink>\`'s \`to\` resolved from the reader's route. */
12
+ export declare function resolveLink(s: Scope, e: Emitter, to: N): void;
13
+ /** \`<RouterLink to="...">\` as vue-router renders it: \`aria-current\` and the active classes when it
14
+ * points where the reader is, then \`href\`, then the link's own class and attributes. */
15
+ export declare function routerLink(s: Scope, e: Emitter, n: N): void;
16
+ /** The routes file: vue-router paths, each a string or `{ "path", "name" }`. */
17
+ export declare function readRoutes(root: string, file: string): RouteDef[];