ferrovue 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/dist/attrs.d.ts +2 -52
- package/dist/attrs.js +18 -448
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +0 -17
- package/dist/client.js +0 -12
- package/dist/compiler.js +25 -90
- package/dist/component.js +10 -86
- package/dist/context.d.ts +4 -66
- package/dist/context.js +2 -33
- package/dist/diff.d.ts +0 -3
- package/dist/diff.js +0 -9
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +27 -890
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +1 -0
- package/dist/islands.js +1 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +14 -143
- package/dist/model.js +27 -12
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +19 -61
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/plugins/scoped.js +125 -0
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +38 -160
- package/dist/script.d.ts +0 -6
- package/dist/script.js +13 -128
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +1 -42
- package/dist/template.js +14 -738
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +1 -23
- package/package.json +7 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -24
- package/dist/scoped.js +0 -177
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -229
package/dist/parens.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
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
1
|
const LOOSE = 0;
|
|
6
2
|
export const OR = 1;
|
|
7
3
|
const AND = 2;
|
|
@@ -10,9 +6,7 @@ const ADD = 4;
|
|
|
10
6
|
const MUL = 5;
|
|
11
7
|
export const AS = 6;
|
|
12
8
|
export const UNARY = 7;
|
|
13
|
-
/** A path, a literal, a call, a macro, a method chain, a field, or something already in brackets. */
|
|
14
9
|
const ATOM = 8;
|
|
15
|
-
/** Binary operators as generated code writes them, one space either side. */
|
|
16
10
|
const BINARY = [
|
|
17
11
|
[/^ \|\| /, OR],
|
|
18
12
|
[/^ && /, AND],
|
|
@@ -21,8 +15,6 @@ const BINARY = [
|
|
|
21
15
|
[/^ [*/%] /, MUL],
|
|
22
16
|
[/^ as /, AS],
|
|
23
17
|
];
|
|
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
18
|
function literalEnd(code, i) {
|
|
27
19
|
if (code[i] === "'") {
|
|
28
20
|
if (code[i + 1] === "\\")
|
|
@@ -39,7 +31,6 @@ function literalEnd(code, i) {
|
|
|
39
31
|
}
|
|
40
32
|
return code.length;
|
|
41
33
|
}
|
|
42
|
-
/** The index of the bracket that closes the one at `i`, or -1. Literals are skipped. */
|
|
43
34
|
function closing(code, i) {
|
|
44
35
|
let depth = 0;
|
|
45
36
|
for (let j = i; j < code.length; j++) {
|
|
@@ -56,8 +47,6 @@ function closing(code, i) {
|
|
|
56
47
|
}
|
|
57
48
|
return -1;
|
|
58
49
|
}
|
|
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
50
|
function topLevel(code) {
|
|
62
51
|
if (/^(?:if|match|let|move|loop|unsafe)\b/.test(code) || code.startsWith("|") || code.startsWith("{"))
|
|
63
52
|
return null;
|
|
@@ -87,8 +76,6 @@ function topLevel(code) {
|
|
|
87
76
|
}
|
|
88
77
|
return ops;
|
|
89
78
|
}
|
|
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
79
|
export function binding(code) {
|
|
93
80
|
const ops = topLevel(code);
|
|
94
81
|
if (!ops)
|
|
@@ -97,64 +84,45 @@ export function binding(code) {
|
|
|
97
84
|
return Math.min(...ops.map((o) => o.p));
|
|
98
85
|
return /^[&*!-]/.test(code) ? UNARY : ATOM;
|
|
99
86
|
}
|
|
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
87
|
export function occurrences(code, name) {
|
|
103
88
|
const text = code.replace(/'(?:[^'\\]|\\u\{[0-9a-f]+\}|\\.)'|"(?:[^"\\]|\\.)*"/g, '""');
|
|
104
89
|
return text.match(new RegExp(`(?<![\\w$])(?<![^.]\\.)${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g"))?.length ?? 0;
|
|
105
90
|
}
|
|
106
|
-
/** `code` as an operand that must bind at least as tightly as `min`. */
|
|
107
91
|
export function operand(code, min) {
|
|
108
92
|
return binding(code) >= min ? code : `(${code})`;
|
|
109
93
|
}
|
|
110
|
-
/** `code` before `.method()` or `.field`. */
|
|
111
94
|
export function atom(code) {
|
|
112
95
|
return operand(code, ATOM);
|
|
113
96
|
}
|
|
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
97
|
export function bare(code) {
|
|
117
98
|
while (code.startsWith("(") && enclosed(code))
|
|
118
99
|
code = code.slice(1, -1);
|
|
119
100
|
return code;
|
|
120
101
|
}
|
|
121
|
-
/** Whether `code` is one bracketed group: `(…)`, `[…]`, `{…}`. */
|
|
122
102
|
export function enclosed(code) {
|
|
123
103
|
return /^[([{]/.test(code) && closing(code, 0) === code.length - 1;
|
|
124
104
|
}
|
|
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
105
|
export function condition(code) {
|
|
128
106
|
const inner = bare(code);
|
|
129
107
|
return inner.startsWith("{") ? `(${inner})` : inner;
|
|
130
108
|
}
|
|
131
|
-
/** A float literal as generated code writes one: `0.5f64`, `2.0f64`, `1e-7f64`. */
|
|
132
109
|
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
110
|
export function binary(a, op, b) {
|
|
137
111
|
const p = BINARY.find(([re]) => re.test(` ${op} `))[1];
|
|
138
112
|
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
113
|
const left = operand(plain(a, b), p === CMP ? p + 1 : p);
|
|
141
114
|
const cast = op === "<" && topLevel(left)?.at(-1)?.op === "as";
|
|
142
115
|
const right = operand(plain(b, a), p + 1);
|
|
143
|
-
// `&x == &y` compares what they borrow, which `x == y` does without the borrows.
|
|
144
116
|
if ((op === "==" || op === "!=") && /^&[^&]/.test(left) && /^&[^&]/.test(right))
|
|
145
117
|
return `${left.slice(1)} ${op} ${right.slice(1)}`;
|
|
146
118
|
return `${cast ? `(${left})` : left} ${op} ${right}`;
|
|
147
119
|
}
|
|
148
|
-
/** `-code`, where `-(-x)` is `x`. */
|
|
149
120
|
export function negate(code) {
|
|
150
121
|
if (code.startsWith("-") && binding(code) === UNARY)
|
|
151
122
|
return code.slice(1);
|
|
152
123
|
return `-${operand(code, UNARY)}`;
|
|
153
124
|
}
|
|
154
|
-
/** Each ordering comparison's opposite, where neither side is NaN. */
|
|
155
125
|
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
126
|
export function not(code) {
|
|
159
127
|
if (code === "true" || code === "false")
|
|
160
128
|
return String(code === "false");
|
|
@@ -167,7 +135,6 @@ export function not(code) {
|
|
|
167
135
|
const eq = cmps.length === 1 && (cmps[0].op === "==" || cmps[0].op === "!=") ? cmps[0] : null;
|
|
168
136
|
if (eq)
|
|
169
137
|
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
138
|
const order = cmps.length === 1 ? cmps[0] : null;
|
|
172
139
|
if (order) {
|
|
173
140
|
const [a, b] = [code.slice(0, order.at), code.slice(order.at + order.op.length + 2)];
|
|
@@ -176,7 +143,6 @@ export function not(code) {
|
|
|
176
143
|
}
|
|
177
144
|
return `!${operand(code, UNARY)}`;
|
|
178
145
|
}
|
|
179
|
-
/** `a && b` and `a || b`, with a constant side folded away. Neither side has effects. */
|
|
180
146
|
export function logical(a, op, b) {
|
|
181
147
|
const absorbing = op === "&&" ? "false" : "true";
|
|
182
148
|
if (a === absorbing || b === absorbing)
|
|
@@ -188,11 +154,9 @@ export function logical(a, op, b) {
|
|
|
188
154
|
return a;
|
|
189
155
|
return binary(a, op, b);
|
|
190
156
|
}
|
|
191
|
-
/** `if test { yes } else { no }` taken apart, or `null` for anything else. */
|
|
192
157
|
export function ifElse(code) {
|
|
193
158
|
if (!code.startsWith("if "))
|
|
194
159
|
return null;
|
|
195
|
-
// The body opens at the first brace outside brackets: a condition holds none of its own.
|
|
196
160
|
let open = -1;
|
|
197
161
|
for (let i = 3; i < code.length && open < 0; i++) {
|
|
198
162
|
const ch = code[i];
|
|
@@ -210,10 +174,7 @@ export function ifElse(code) {
|
|
|
210
174
|
return null;
|
|
211
175
|
return { test: code.slice(3, open - 1), yes: code.slice(open + 2, close - 1), no: code.slice(close + 9, -2) };
|
|
212
176
|
}
|
|
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
177
|
export function strArg(code) {
|
|
216
|
-
// Each branch of an `if` is coerced on its own.
|
|
217
178
|
const branches = ifElse(code);
|
|
218
179
|
if (branches)
|
|
219
180
|
return `if ${branches.test} { ${strArg(branches.yes)} } else { ${strArg(branches.no)} }`;
|
|
@@ -222,17 +183,12 @@ export function strArg(code) {
|
|
|
222
183
|
return code;
|
|
223
184
|
return m[1] ? m[2] : `&${m[2]}`;
|
|
224
185
|
}
|
|
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
186
|
export function callOn(recv, call) {
|
|
228
187
|
const branches = ifElse(recv);
|
|
229
188
|
if (branches)
|
|
230
189
|
return `if ${branches.test} { ${callOn(branches.yes, call)} } else { ${callOn(branches.no, call)} }`;
|
|
231
190
|
return `${receiver(recv)}.${call}`;
|
|
232
191
|
}
|
|
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
192
|
export function receiver(code) {
|
|
237
193
|
const m = /^[&*]+(.*)$/s.exec(code);
|
|
238
194
|
return m && binding(m[1]) === ATOM ? m[1] : atom(code);
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { SFCDescriptor } from "@vue/compiler-sfc";
|
|
2
|
+
import { type Component, type N, type Scope, type Struct, type StructTy, type Ty, type Val } from "./model.ts";
|
|
3
|
+
import { type Config } from "./context.ts";
|
|
4
|
+
import { type Emitter } from "./emitter.ts";
|
|
5
|
+
import type { Presence } from "./narrowing.ts";
|
|
6
|
+
export interface RenderParam {
|
|
7
|
+
name: string;
|
|
8
|
+
ty: string;
|
|
9
|
+
pageTy: string;
|
|
10
|
+
reads(comp: Component): boolean;
|
|
11
|
+
test: {
|
|
12
|
+
lines: string[];
|
|
13
|
+
arg: string;
|
|
14
|
+
fixture?: boolean;
|
|
15
|
+
after?: string[];
|
|
16
|
+
};
|
|
17
|
+
fixtureField?: string;
|
|
18
|
+
fixtureDefault?: string;
|
|
19
|
+
testSupport?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SlotField {
|
|
22
|
+
js: string;
|
|
23
|
+
rust: string;
|
|
24
|
+
doc: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ValueHooks {
|
|
27
|
+
describe?(ty: Ty): string | undefined;
|
|
28
|
+
truthy?(v: Val): string | undefined;
|
|
29
|
+
interpolate?(e: Emitter, v: Val): boolean;
|
|
30
|
+
unbindable?(ty: Ty): {
|
|
31
|
+
what: string;
|
|
32
|
+
fix: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
nullish?(s: Scope, a: Val, b: Val, n: N): Val | undefined;
|
|
35
|
+
equals?(a: Val, b: Val): string | undefined;
|
|
36
|
+
isArray?(v: Val): Val | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface ReadComponent {
|
|
39
|
+
comp: Component;
|
|
40
|
+
ssr: string;
|
|
41
|
+
children: Map<string, string>;
|
|
42
|
+
attrsBindings: Set<string>;
|
|
43
|
+
}
|
|
44
|
+
export interface Plugin<Run = unknown, Local = unknown> {
|
|
45
|
+
name: string;
|
|
46
|
+
configure?(config: Config, root: string): Run;
|
|
47
|
+
prepare?(root: string): void;
|
|
48
|
+
sfc?(comp: Component, descriptor: SFCDescriptor, file: string, source: string): void;
|
|
49
|
+
templateOptions?(comp: Component): {
|
|
50
|
+
id: string;
|
|
51
|
+
scoped: boolean;
|
|
52
|
+
slotted: boolean;
|
|
53
|
+
};
|
|
54
|
+
importedType?(comp: Component, file: string, name: string, local: string): boolean;
|
|
55
|
+
struct?(ty: StructTy): {
|
|
56
|
+
st: Struct | undefined;
|
|
57
|
+
module: string;
|
|
58
|
+
} | null;
|
|
59
|
+
compiled?(comp: Component, code: string, script: N[]): void;
|
|
60
|
+
analyse?(read: ReadComponent[]): void;
|
|
61
|
+
scope?(s: Scope): Local;
|
|
62
|
+
scriptImport?(s: Scope, st: N, from: string): boolean;
|
|
63
|
+
scriptBinding?(s: Scope, d: N): boolean;
|
|
64
|
+
global?(s: Scope, name: string, n: N): Val | null;
|
|
65
|
+
call?(s: Scope, n: N): Val | null;
|
|
66
|
+
member?(s: Scope, base: Val, prop: string, n: N, computed: boolean): Val | null;
|
|
67
|
+
equality?(s: Scope, n: N): Val | null;
|
|
68
|
+
presence?(s: Scope, n: N): Presence | null | undefined;
|
|
69
|
+
values?: ValueHooks;
|
|
70
|
+
resolveComponent?(s: Scope, local: string, name: string): boolean;
|
|
71
|
+
component?(s: Scope, e: Emitter, n: N): boolean;
|
|
72
|
+
child?(s: Scope, child: Component, n: N): void;
|
|
73
|
+
childIds?(s: Scope, child: Component, passesAttrs: boolean, inSlot: boolean, n: N): string | null;
|
|
74
|
+
statement?(s: Scope, e: Emitter, call: N, st: N): boolean;
|
|
75
|
+
params?: RenderParam[];
|
|
76
|
+
slotFields?(comp: Component): SlotField[];
|
|
77
|
+
modules?(): [string, string][];
|
|
78
|
+
}
|
|
79
|
+
export declare function runOf<Run>(plugin: Plugin<Run>): Run;
|
|
80
|
+
export declare function scopeOf<Local>(plugin: Plugin<unknown, Local>, s: Scope): Local;
|
|
81
|
+
export declare function claim<T>(ask: (p: Plugin) => T | null | undefined): T | undefined;
|
|
82
|
+
export declare function renderParams(): RenderParam[];
|
|
83
|
+
export declare function paramsOf(comp: Component): RenderParam[];
|
|
84
|
+
export declare function slotFieldsOf(comp: Component): SlotField[];
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {} from "./model.js";
|
|
2
|
+
import { ctx } from "./context.js";
|
|
3
|
+
import {} from "./emitter.js";
|
|
4
|
+
export function runOf(plugin) {
|
|
5
|
+
return ctx.runs.get(plugin);
|
|
6
|
+
}
|
|
7
|
+
export function scopeOf(plugin, s) {
|
|
8
|
+
return s.plugins.get(plugin);
|
|
9
|
+
}
|
|
10
|
+
export function claim(ask) {
|
|
11
|
+
for (const p of ctx.plugins) {
|
|
12
|
+
const answer = ask(p);
|
|
13
|
+
if (answer !== null && answer !== undefined)
|
|
14
|
+
return answer;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function renderParams() {
|
|
19
|
+
return ctx.plugins.flatMap((p) => p.params ?? []);
|
|
20
|
+
}
|
|
21
|
+
export function paramsOf(comp) {
|
|
22
|
+
return renderParams().filter((p) => comp.takes.has(p.name));
|
|
23
|
+
}
|
|
24
|
+
export function slotFieldsOf(comp) {
|
|
25
|
+
return ctx.plugins.flatMap((p) => p.slotFields?.(comp) ?? []);
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Component, type N } from "../model.ts";
|
|
2
|
+
import { type Plugin } from "../plugin.ts";
|
|
3
|
+
export interface LocaleMessages {
|
|
4
|
+
name: string;
|
|
5
|
+
file: string;
|
|
6
|
+
messages: Map<string, N>;
|
|
7
|
+
}
|
|
8
|
+
export interface I18nSetup {
|
|
9
|
+
dir: string;
|
|
10
|
+
locales: LocaleMessages[];
|
|
11
|
+
locale: string;
|
|
12
|
+
fallback: string[];
|
|
13
|
+
}
|
|
14
|
+
interface I18nRun {
|
|
15
|
+
setup: I18nSetup | null;
|
|
16
|
+
readers: Set<Component>;
|
|
17
|
+
}
|
|
18
|
+
interface I18nScope {
|
|
19
|
+
useI18n: string | null;
|
|
20
|
+
t: Set<string>;
|
|
21
|
+
}
|
|
22
|
+
export declare const i18n: Plugin<I18nRun, I18nScope>;
|
|
23
|
+
export {};
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
/* vue-i18n: the locale files read and compiled to Rust tables, and `$t` / `useI18n().t` calls. */
|
|
2
1
|
import { readdirSync, readFileSync } from "node:fs";
|
|
3
2
|
import { basename, join } from "node:path";
|
|
4
3
|
import { createParser } from "@intlify/message-compiler";
|
|
5
|
-
import { fail, GenError, rustStr, STR } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
/** The modifiers vue-i18n defines for linked messages; any other would throw in the browser. */
|
|
4
|
+
import { fail, GenError, rustStr, STR } from "../model.js";
|
|
5
|
+
import { expr } from "../expr.js";
|
|
6
|
+
import { bare, strArg } from "../parens.js";
|
|
7
|
+
import { header } from "../rust.js";
|
|
8
|
+
import { runOf, scopeOf } from "../plugin.js";
|
|
11
9
|
const MODIFIERS = new Set(["upper", "lower", "capitalize"]);
|
|
12
|
-
|
|
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) {
|
|
10
|
+
function readLocales(root, config) {
|
|
15
11
|
const dir = join(root, config.messages);
|
|
16
12
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json")).toSorted();
|
|
17
13
|
const parser = createParser({});
|
|
@@ -30,7 +26,6 @@ export function readLocales(root, config) {
|
|
|
30
26
|
const walk = (value, path) => {
|
|
31
27
|
if (typeof value === "string") {
|
|
32
28
|
const key = path.join(".");
|
|
33
|
-
// A key spelled with dots is reached only when no nested path is.
|
|
34
29
|
(path.length > 1 ? nested : flat).set(key, value);
|
|
35
30
|
}
|
|
36
31
|
else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -52,7 +47,6 @@ export function readLocales(root, config) {
|
|
|
52
47
|
const fallback = config.fallbackLocale === undefined ? [] : Array.isArray(config.fallbackLocale) ? config.fallbackLocale : [config.fallbackLocale];
|
|
53
48
|
return { dir: config.messages, locales, locale: config.locale ?? "en", fallback };
|
|
54
49
|
}
|
|
55
|
-
/** A message's AST as the Rust `Message` literal: its cases, each a list of `Part`s. */
|
|
56
50
|
function messageSource(file, key, ast) {
|
|
57
51
|
const body = ast.body;
|
|
58
52
|
const cases = body.type === 1 ? body.cases : [body];
|
|
@@ -82,11 +76,10 @@ function messageSource(file, key, ast) {
|
|
|
82
76
|
const items = (c) => c.items ?? (c.static !== undefined ? [{ type: 3, value: c.static }] : []);
|
|
83
77
|
return `Message { cases: &[${cases.map((c) => `&[${items(c).map(part).join(", ")}]`).join(", ")}] }`;
|
|
84
78
|
}
|
|
85
|
-
/** Keys in the order Rust's `str` sorts them — byte order — for the table's binary search. */
|
|
86
79
|
function byteOrder(a, b) {
|
|
87
80
|
return Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
88
81
|
}
|
|
89
|
-
|
|
82
|
+
function i18nSource(setup) {
|
|
90
83
|
const locales = setup.locales
|
|
91
84
|
.map((l) => {
|
|
92
85
|
const keys = [...l.messages.keys()].toSorted(byteOrder);
|
|
@@ -121,7 +114,6 @@ pub fn i18n(locale: &str) -> ferrovue::I18n {
|
|
|
121
114
|
}
|
|
122
115
|
`;
|
|
123
116
|
}
|
|
124
|
-
/** A value interpolated into a message, as \`ferrovue::i18n::Value\`. */
|
|
125
117
|
function i18nValue(s, n) {
|
|
126
118
|
const v = expr(s, n);
|
|
127
119
|
switch (v.ty.k) {
|
|
@@ -137,10 +129,8 @@ function i18nValue(s, n) {
|
|
|
137
129
|
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
130
|
}
|
|
139
131
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
export function translate(s, args, n) {
|
|
143
|
-
if (!ctx.i18n)
|
|
132
|
+
function translate(s, args, n) {
|
|
133
|
+
if (!runOf(i18n).setup)
|
|
144
134
|
fail(s.comp, "`t()` needs `i18n` in ferrovue.config.json: where the locale files are", n);
|
|
145
135
|
if (args.length < 1 || args.length > 3)
|
|
146
136
|
fail(s.comp, "`t()` takes a key, then named values, a list or a plural number", n);
|
|
@@ -173,3 +163,70 @@ export function translate(s, args, n) {
|
|
|
173
163
|
}
|
|
174
164
|
return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args { named: ${named}, list: ${list}, plural: ${plural} })`, ty: STR };
|
|
175
165
|
}
|
|
166
|
+
export const i18n = {
|
|
167
|
+
name: "i18n",
|
|
168
|
+
configure: (config, root) => ({ setup: config.i18n ? readLocales(root, config.i18n) : null, readers: new Set() }),
|
|
169
|
+
compiled(comp, code) {
|
|
170
|
+
if (code.includes("_ctx.$t("))
|
|
171
|
+
runOf(i18n).readers.add(comp);
|
|
172
|
+
},
|
|
173
|
+
scope: () => ({ useI18n: null, t: new Set() }),
|
|
174
|
+
scriptImport(s, st, from) {
|
|
175
|
+
if (from !== "vue-i18n")
|
|
176
|
+
return false;
|
|
177
|
+
for (const sp of st.specifiers) {
|
|
178
|
+
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
179
|
+
if (name === "useI18n")
|
|
180
|
+
scopeOf(i18n, s).useI18n = sp.local.name;
|
|
181
|
+
else
|
|
182
|
+
s.clientOnly.set(sp.local.name, `\`${name}\` from vue-i18n does not run on the server`);
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
scriptBinding(s, d) {
|
|
187
|
+
const own = scopeOf(i18n, s);
|
|
188
|
+
const init = d.init;
|
|
189
|
+
if (own.useI18n === null || d.id.type !== "ObjectPattern" || init?.type !== "CallExpression" || init.callee.type !== "Identifier" || init.callee.name !== own.useI18n) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
for (const p of d.id.properties) {
|
|
193
|
+
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
194
|
+
fail(s.comp, "`useI18n()` is destructured into plain names", p);
|
|
195
|
+
}
|
|
196
|
+
const key = p.key.name ?? p.key.value;
|
|
197
|
+
if (key === "t")
|
|
198
|
+
own.t.add(p.value.name);
|
|
199
|
+
else if (key === "locale") {
|
|
200
|
+
s.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
|
|
201
|
+
s.refs.add(p.value.name);
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
s.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
|
|
205
|
+
}
|
|
206
|
+
runOf(i18n).readers.add(s.comp);
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
call(s, n) {
|
|
210
|
+
const callee = n.callee;
|
|
211
|
+
const { t } = scopeOf(i18n, s);
|
|
212
|
+
const translates = callee.type === "Identifier"
|
|
213
|
+
? t.has(callee.name)
|
|
214
|
+
: callee.object.type === "Identifier" &&
|
|
215
|
+
((callee.object.name === "_ctx" && callee.property.name === "$t") || (callee.object.name === "$setup" && t.has(callee.property.name)));
|
|
216
|
+
return translates ? translate(s, n.arguments, n) : null;
|
|
217
|
+
},
|
|
218
|
+
params: [
|
|
219
|
+
{
|
|
220
|
+
name: "fv_i18n",
|
|
221
|
+
ty: "&fv::I18n",
|
|
222
|
+
pageTy: "&'p fv::I18n",
|
|
223
|
+
reads: (c) => runOf(i18n).readers.has(c),
|
|
224
|
+
test: { lines: ["let i18n = i18n::i18n(fixture.locale.as_deref().unwrap_or(i18n::LOCALE));"], arg: "&i18n", fixture: true },
|
|
225
|
+
fixtureField: ' #[serde(rename = "$locale", default)]\n locale: Option<String>,',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
modules() {
|
|
229
|
+
const { setup } = runOf(i18n);
|
|
230
|
+
return setup ? [["i18n.rs", i18nSource(setup)]] : [];
|
|
231
|
+
},
|
|
232
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {} from "../plugin.js";
|
|
2
|
+
import { router } from "./router.js";
|
|
3
|
+
import { piniaStores } from "./stores.js";
|
|
4
|
+
import { sharedTypes } from "./shared-types.js";
|
|
5
|
+
import { i18n } from "./i18n.js";
|
|
6
|
+
import { teleport } from "./teleport.js";
|
|
7
|
+
import { scoped } from "./scoped.js";
|
|
8
|
+
export const PLUGINS = [router, piniaStores, sharedTypes, i18n, teleport, scoped];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type N, type Scope, type Val } from "../model.ts";
|
|
2
|
+
import { Emitter } from "../emitter.ts";
|
|
3
|
+
export declare const ROUTER_LINK_PROPS: Set<string>;
|
|
4
|
+
export declare const ROUTER_LINK_INERT: Set<string>;
|
|
5
|
+
export declare function routeParams(path: string): string[];
|
|
6
|
+
export declare function urlText(s: Scope, v: Val, n: N): string;
|
|
7
|
+
export declare function resolveLink(s: Scope, e: Emitter, to: N): void;
|
|
8
|
+
export declare function routerLink(s: Scope, e: Emitter, n: N): void;
|
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
/* `<RouterLink>`, its locations, and the routes file. */
|
|
2
1
|
import { escapeHtml } from "@vue/shared";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
2
|
+
import { camelize, fail, rustStr } from "../model.js";
|
|
3
|
+
import { CONFIG_FILE } from "../context.js";
|
|
4
|
+
import { expr } from "../expr.js";
|
|
5
|
+
import { lonely } from "../strings.js";
|
|
6
|
+
import { bare, condition, strArg } from "../parens.js";
|
|
7
|
+
import { Emitter } from "../emitter.js";
|
|
8
|
+
import { attrOf, dollarAttrs, IGNORED_PROPS, isAttrs, mergedParts, mergeProps, renderDynamicAttr } from "../attrs.js";
|
|
9
|
+
import { renderStyle } from "../styles.js";
|
|
10
|
+
import { classItems } from "../classes.js";
|
|
11
|
+
import { statements } from "../template.js";
|
|
12
|
+
import { slotBody } from "../slots.js";
|
|
13
|
+
import { runOf } from "../plugin.js";
|
|
14
|
+
import { scopeIdOf } from "./scoped.js";
|
|
15
|
+
import { allRoutes, router } from "./router.js";
|
|
12
16
|
export const ROUTER_LINK_PROPS = new Set(["to", "class", "activeClass", "exactActiveClass", "ariaCurrentValue"]);
|
|
13
|
-
/** `<RouterLink>` props that change only what a click does, which the server does not render. */
|
|
14
17
|
export const ROUTER_LINK_INERT = new Set(["replace", "viewTransition"]);
|
|
15
|
-
/** The parameter names of a route path, in order. */
|
|
16
18
|
export function routeParams(path) {
|
|
17
19
|
return [...path.matchAll(/:(\w+)/g)].map((m) => m[1]);
|
|
18
20
|
}
|
|
19
|
-
/** A string written into a link, which vue-router percent-encodes: half of a surrogate pair makes
|
|
20
|
-
* `encodeURI` throw, so Vue renders no page at all. */
|
|
21
21
|
function noHalves(s, v, n) {
|
|
22
22
|
if (v.lone)
|
|
23
23
|
fail(s.comp, lonely("a `<RouterLink>` location"), n);
|
|
24
24
|
}
|
|
25
|
-
/** A value written into a URL — a parameter or a query value — as the `&str` vue-router stringifies. */
|
|
26
25
|
export function urlText(s, v, n) {
|
|
27
26
|
noHalves(s, v, n);
|
|
28
27
|
if (v.ty.k === "str")
|
|
@@ -33,7 +32,6 @@ export function urlText(s, v, n) {
|
|
|
33
32
|
return `if ${condition(v.code)} { "true" } else { "false" }`;
|
|
34
33
|
return fail(s.comp, "a route parameter is a string or a number that is present", n);
|
|
35
34
|
}
|
|
36
|
-
/** \`let fv_link = …;\`: a \`<RouterLink>\`'s \`to\` resolved from the reader's route. */
|
|
37
35
|
export function resolveLink(s, e, to) {
|
|
38
36
|
if (to.type !== "ObjectExpression") {
|
|
39
37
|
const target = expr(s, to);
|
|
@@ -56,7 +54,6 @@ export function resolveLink(s, e, to) {
|
|
|
56
54
|
}
|
|
57
55
|
e.open("let fv_link =");
|
|
58
56
|
const query = parts.get("query");
|
|
59
|
-
// The query, built only when the location has one.
|
|
60
57
|
const search = query ? "&fv_search" : '""';
|
|
61
58
|
if (query) {
|
|
62
59
|
e.stmt("let mut fv_search = String::new();");
|
|
@@ -67,7 +64,6 @@ export function resolveLink(s, e, to) {
|
|
|
67
64
|
fail(s.comp, "a query holds plain keys", p);
|
|
68
65
|
const key = String(p.key.name ?? p.key.value);
|
|
69
66
|
const v = expr(s, p.value);
|
|
70
|
-
// An absent value leaves its key out, as `undefined` does.
|
|
71
67
|
if (v.ty.k === "undef")
|
|
72
68
|
continue;
|
|
73
69
|
if (v.ty.k === "opt") {
|
|
@@ -93,7 +89,7 @@ export function resolveLink(s, e, to) {
|
|
|
93
89
|
if (name) {
|
|
94
90
|
if (name.type !== "StringLiteral")
|
|
95
91
|
fail(s.comp, "a `to`'s `name` is a string literal, checked against the routes", name);
|
|
96
|
-
const route = allRoutes(
|
|
92
|
+
const route = allRoutes(runOf(router).routes).find((r) => r.name === name.value);
|
|
97
93
|
if (!route)
|
|
98
94
|
fail(s.comp, `no route is called \`${name.value}\``, name);
|
|
99
95
|
const wanted = routeParams(route.fullPath);
|
|
@@ -130,8 +126,6 @@ export function resolveLink(s, e, to) {
|
|
|
130
126
|
fail(s.comp, "a `to` object has a `name` or a `path`", to);
|
|
131
127
|
e.close(";");
|
|
132
128
|
}
|
|
133
|
-
/** \`<RouterLink to="...">\` as vue-router renders it: \`aria-current\` and the active classes when it
|
|
134
|
-
* points where the reader is, then \`href\`, then the link's own class and attributes. */
|
|
135
129
|
export function routerLink(s, e, n) {
|
|
136
130
|
const [, rawProps, slots, , slotScopeId] = n.arguments;
|
|
137
131
|
let props = rawProps;
|
|
@@ -161,18 +155,11 @@ export function routerLink(s, e, n) {
|
|
|
161
155
|
const to = fields.get("to");
|
|
162
156
|
if (!to)
|
|
163
157
|
fail(s.comp, "`<RouterLink>` needs `to`", n);
|
|
164
|
-
/* vue-router renders the link from virtual nodes (`renderElementVNode`). The `<a>` takes the ids
|
|
165
|
-
* of a component's root after its attributes: what the parent passes on when the link is its
|
|
166
|
-
* root, the id of the component that wrote it, and the slot scope ids around it — as
|
|
167
|
-
* `scope_attrs` gathers them. Its content takes that component's id, which the compiled template
|
|
168
|
-
* spells out, and the slot scope ids again, each once: unlike the raw `_scopeId` the compiled
|
|
169
|
-
* elements would write, so an element there is refused when there are some. */
|
|
170
158
|
const passed = (rawProps?.type === "Identifier" && rawProps.name === "_attrs") || rawProps?.arguments?.some((a) => a.type === "Identifier" && a.name === "_attrs");
|
|
171
159
|
const base = passed ? s.attrs : null;
|
|
172
160
|
const slotted = slotScopeId ? s.sid : null;
|
|
173
|
-
const scopeId = s.comp
|
|
161
|
+
const scopeId = scopeIdOf(s.comp);
|
|
174
162
|
const content = slots?.type === "ObjectExpression" ? slots.properties.filter((p) => (p.key?.name ?? p.key?.value) !== "_").map((p) => p.value) : [];
|
|
175
|
-
/** Whether the link's content holds a node that `is` picks out. */
|
|
176
163
|
const holds = (is) => {
|
|
177
164
|
const within = (x) => !!x && typeof x === "object" && (is(x) || Object.values(x).some(within));
|
|
178
165
|
return content.some((c) => slotBody(s, c).some(within));
|
|
@@ -181,13 +168,11 @@ export function routerLink(s, e, n) {
|
|
|
181
168
|
if (slotted !== null && holds(writesScopeId)) {
|
|
182
169
|
fail(s.comp, "an element inside a `<RouterLink>` in slot content given a slot scope id: vue-router writes those ids by rules of its own", to);
|
|
183
170
|
}
|
|
184
|
-
// A `<slot>` there is rendered as a virtual node too, which passes its own slot scope ids.
|
|
185
171
|
if ((scopeId !== null || slotted !== null || base !== null) && holds((x) => x.type === "CallExpression" && x.callee.name === "_ssrRenderSlot")) {
|
|
186
172
|
fail(s.comp, "a `<slot>` inside a `<RouterLink>` that takes scope ids: vue-router renders it by rules of its own", to);
|
|
187
173
|
}
|
|
188
|
-
if (!
|
|
174
|
+
if (!runOf(router).routes)
|
|
189
175
|
fail(s.comp, `\`<RouterLink>\` needs \`routes\` in ${CONFIG_FILE}: the paths it resolves against`, n);
|
|
190
|
-
/** A literal-string prop, which the class names and `aria-current` must be. */
|
|
191
176
|
const literal = (key, fallback) => {
|
|
192
177
|
const v = fields.get(key);
|
|
193
178
|
if (!v)
|
|
@@ -196,14 +181,12 @@ export function routerLink(s, e, n) {
|
|
|
196
181
|
fail(s.comp, `\`${key}\` on \`<RouterLink>\` is a string literal`, v);
|
|
197
182
|
return v.value;
|
|
198
183
|
};
|
|
199
|
-
const activeClass = literal("activeClass",
|
|
200
|
-
const exactClass = literal("exactActiveClass",
|
|
184
|
+
const activeClass = literal("activeClass", runOf(router).linkActive);
|
|
185
|
+
const exactClass = literal("exactActiveClass", runOf(router).linkExactActive);
|
|
201
186
|
const ariaCurrent = literal("ariaCurrentValue", "page");
|
|
202
187
|
e.open("");
|
|
203
188
|
resolveLink(s, e, to);
|
|
204
189
|
e.lit("<a");
|
|
205
|
-
/* Attributes this component is passed fall through to the link: merged into its virtual node's
|
|
206
|
-
* props with the template's, then onto the `<a>` vue-router renders. */
|
|
207
190
|
const parts = mergedParts(rawProps);
|
|
208
191
|
const passedOn = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
209
192
|
const fallsThrough = parts.some(passedOn);
|
|
@@ -224,8 +207,6 @@ export function routerLink(s, e, n) {
|
|
|
224
207
|
e.lit(' href="');
|
|
225
208
|
e.stmt("fv::escape_into(out, &fv_link.href);");
|
|
226
209
|
e.lit('" class="');
|
|
227
|
-
// `{ [activeClass]: isActive, [exactActiveClass]: isExactActive }`. When the two are one class,
|
|
228
|
-
// the later key wins: the class goes with `isExactActive`.
|
|
229
210
|
const linkItems = activeClass === exactClass
|
|
230
211
|
? [`if fv_link.exact { ${rustStr(exactClass)} } else { "" }`]
|
|
231
212
|
: [`if fv_link.active { ${rustStr(activeClass)} } else { "" }`, `if fv_link.exact { ${rustStr(exactClass)} } else { "" }`];
|
|
@@ -251,7 +232,6 @@ export function routerLink(s, e, n) {
|
|
|
251
232
|
e.stmt(`out.push_str(&fv::scope_attrs(${base ?? '""'}, ${scopeId === null ? '""' : rustStr(scopeId)}, ${slotted ?? '""'}));`);
|
|
252
233
|
if (fallsThrough) {
|
|
253
234
|
e.close(" else {");
|
|
254
|
-
// The template's attributes for the link, merged in their order with those passed on.
|
|
255
235
|
const sources = [];
|
|
256
236
|
for (const p of parts) {
|
|
257
237
|
if (passedOn(p))
|
|
@@ -274,7 +254,6 @@ export function routerLink(s, e, n) {
|
|
|
274
254
|
const ids = base === null && slotted === null
|
|
275
255
|
? rustStr(scopeId === null ? "" : ` ${scopeId}`)
|
|
276
256
|
: `&fv::scope_attrs(${base ?? '""'}, ${scopeId === null ? '""' : rustStr(scopeId)}, ${slotted ?? '""'})`;
|
|
277
|
-
// vue-router's own: `aria-current` (`null` when the link is not exact), `href`, its classes.
|
|
278
257
|
const linkOwn = [
|
|
279
258
|
`("aria-current", if fv_link.exact { fv::Attr::str(${rustStr(ariaCurrent)}) } else { fv::Attr::Undefined })`,
|
|
280
259
|
'("href", fv::Attr::str(&fv_link.href))',
|
|
@@ -299,24 +278,3 @@ export function routerLink(s, e, n) {
|
|
|
299
278
|
e.lit("</a>");
|
|
300
279
|
e.close();
|
|
301
280
|
}
|
|
302
|
-
/** The routes file: vue-router paths, each a string or `{ "path", "name" }`. */
|
|
303
|
-
export function readRoutes(root, file) {
|
|
304
|
-
const raw = JSON.parse(readFileSync(join(root, file), "utf8"));
|
|
305
|
-
if (!Array.isArray(raw))
|
|
306
|
-
throw new GenError(`${file} lists the routes in an array`);
|
|
307
|
-
const read = (list, parent) => list.map((r) => {
|
|
308
|
-
const o = (typeof r === "string" ? { path: r } : r);
|
|
309
|
-
if (typeof o?.path !== "string" || (o.name !== undefined && typeof o.name !== "string") || (o.children !== undefined && !Array.isArray(o.children))) {
|
|
310
|
-
throw new GenError(`${file}: a route is a path, or \`{ "path": "…", "name": "…", "children": [ … ] }\``);
|
|
311
|
-
}
|
|
312
|
-
// A child's path joins its parent's unless it starts with `/`; an empty one is the parent's.
|
|
313
|
-
const fullPath = parent === null || o.path.startsWith("/") ? o.path : `${parent}${parent.endsWith("/") || o.path === "" ? "" : "/"}${o.path}`;
|
|
314
|
-
const def = { path: o.path, fullPath };
|
|
315
|
-
if (o.name !== undefined)
|
|
316
|
-
def.name = o.name;
|
|
317
|
-
if (o.children)
|
|
318
|
-
def.children = read(o.children, fullPath);
|
|
319
|
-
return def;
|
|
320
|
-
});
|
|
321
|
-
return read(raw, null);
|
|
322
|
-
}
|