ferrovue 0.7.2 → 0.9.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 +154 -691
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +146 -13
- package/dist/children.js +8 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +25 -3
- package/dist/compiler.js +5 -2
- package/dist/component.js +3 -2
- package/dist/context.d.ts +7 -0
- package/dist/context.js +4 -1
- package/dist/errors.d.ts +116 -5
- package/dist/errors.js +81 -6
- package/dist/expr.js +143 -30
- package/dist/fixture.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lists.d.ts +9 -0
- package/dist/lists.js +421 -24
- package/dist/model.d.ts +1 -0
- package/dist/narrowing.d.ts +9 -0
- package/dist/narrowing.js +98 -9
- package/dist/numbers.d.ts +4 -0
- package/dist/numbers.js +61 -2
- package/dist/parens.js +7 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugins/dates.d.ts +25 -0
- package/dist/plugins/dates.js +224 -0
- package/dist/plugins/i18n.js +16 -14
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/router.js +8 -5
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +22 -9
- package/dist/schema.js +1 -0
- package/dist/script.d.ts +1 -0
- package/dist/script.js +41 -32
- package/dist/strings.d.ts +9 -1
- package/dist/strings.js +113 -23
- package/dist/styles.d.ts +3 -0
- package/dist/styles.js +22 -0
- package/dist/typescript.d.ts +12 -4
- package/dist/typescript.js +50 -12
- package/package.json +20 -20
- package/schema.json +6 -0
package/dist/narrowing.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { absence, BOOL, fail, nothing } from "./model.js";
|
|
1
|
+
import { absence, BOOL, fail, nothing, rustStr, STR } from "./model.js";
|
|
2
2
|
import { ctx } from "./context.js";
|
|
3
3
|
import { claim } from "./plugin.js";
|
|
4
4
|
import { atom, bare, binary, condition, ifElse, logical, not, occurrences, receiver } from "./parens.js";
|
|
5
5
|
import { isNumber, negatedOrder } from "./numbers.js";
|
|
6
|
-
import { expr } from "./expr.js";
|
|
6
|
+
import { describeTy, expr } from "./expr.js";
|
|
7
7
|
export function known(v) {
|
|
8
8
|
if (v.konst !== undefined)
|
|
9
9
|
return v.konst;
|
|
@@ -34,8 +34,10 @@ export function truthy(v) {
|
|
|
34
34
|
case "undef":
|
|
35
35
|
case "null":
|
|
36
36
|
return "false";
|
|
37
|
-
case "opt":
|
|
38
|
-
|
|
37
|
+
case "opt": {
|
|
38
|
+
const each = truthy({ code: "v", ty: v.ty.of });
|
|
39
|
+
return each === "true" ? `${atom(v.code)}.is_some()` : `${atom(v.code)}.is_some_and(|v| ${each})`;
|
|
40
|
+
}
|
|
39
41
|
default:
|
|
40
42
|
return claim((p) => p.values?.truthy?.(v)) ?? "true";
|
|
41
43
|
}
|
|
@@ -52,6 +54,8 @@ export function pathOf(n) {
|
|
|
52
54
|
return `${base}.${n.property.name}`;
|
|
53
55
|
if (n.computed && n.property.type === "StringLiteral")
|
|
54
56
|
return `${base}.${n.property.value}`;
|
|
57
|
+
if (n.computed && n.property.type === "NumericLiteral")
|
|
58
|
+
return `${base}[${n.property.value}]`;
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
57
61
|
function nothingLiteral(n) {
|
|
@@ -83,6 +87,72 @@ export function checkNullTest(s, t, v) {
|
|
|
83
87
|
const what = none === "null" ? "is `T | null`, never `undefined`" : "is optional, which is `undefined` when absent, never `null`";
|
|
84
88
|
fail(s.comp, "FV0627", `\`${op}= ${t.literal}\` of a value that ${what}: compare with \`${other}\``, t.target);
|
|
85
89
|
}
|
|
90
|
+
export function typeofCompare(n) {
|
|
91
|
+
if (n.type !== "BinaryExpression" || !["===", "!==", "==", "!="].includes(n.operator))
|
|
92
|
+
return null;
|
|
93
|
+
const isTypeof = (m) => m.type === "UnaryExpression" && m.operator === "typeof";
|
|
94
|
+
const [t, lit] = isTypeof(n.left) && n.right.type === "StringLiteral" ? [n.left, n.right] : isTypeof(n.right) && n.left.type === "StringLiteral" ? [n.right, n.left] : [null, null];
|
|
95
|
+
return t ? { target: t.argument, literal: lit.value, equal: n.operator.startsWith("=") } : null;
|
|
96
|
+
}
|
|
97
|
+
function typeName(s, ty, n) {
|
|
98
|
+
switch (ty.k) {
|
|
99
|
+
case "str":
|
|
100
|
+
case "html":
|
|
101
|
+
return "string";
|
|
102
|
+
case "int":
|
|
103
|
+
case "float":
|
|
104
|
+
return "number";
|
|
105
|
+
case "bool":
|
|
106
|
+
return "boolean";
|
|
107
|
+
case "undef":
|
|
108
|
+
return "undefined";
|
|
109
|
+
case "null":
|
|
110
|
+
case "list":
|
|
111
|
+
case "record":
|
|
112
|
+
case "struct":
|
|
113
|
+
case "child":
|
|
114
|
+
case "date":
|
|
115
|
+
return "object";
|
|
116
|
+
default:
|
|
117
|
+
return fail(s.comp, "FV0722", `\`typeof\` of ${describeTy(ty)}, whose JavaScript type the compiler does not know`, n);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** What `typeof v` gives: `present` for a value that is there, and `absent` for an optional one
|
|
121
|
+
* that is not (`null` when `v` cannot be absent, `either` when it may be `null` or `undefined`). */
|
|
122
|
+
function typeNames(s, v, n) {
|
|
123
|
+
if (v.ty.k !== "opt")
|
|
124
|
+
return { present: typeName(s, v.ty, n), absent: null };
|
|
125
|
+
const absent = v.ty.none === "null" ? "object" : v.ty.none === "either" ? "either" : "undefined";
|
|
126
|
+
return { present: typeName(s, v.ty.of, n), absent };
|
|
127
|
+
}
|
|
128
|
+
function eitherRefused(s, literal, n) {
|
|
129
|
+
return fail(s.comp, "FV0722", `\`typeof\` compared with "${literal}" of a value that may be \`null\` (an "object") or \`undefined\`, which its Rust \`Option\` cannot tell apart: test it with \`== null\``, n);
|
|
130
|
+
}
|
|
131
|
+
export function typeofVal(s, n) {
|
|
132
|
+
const v = expr(s, n.argument);
|
|
133
|
+
const { present, absent } = typeNames(s, v, n.argument);
|
|
134
|
+
if (absent === null || absent === present)
|
|
135
|
+
return { code: rustStr(present), ty: STR };
|
|
136
|
+
if (absent === "either")
|
|
137
|
+
return eitherRefused(s, present, n);
|
|
138
|
+
return { code: `if ${atom(v.code)}.is_some() { ${rustStr(present)} } else { ${rustStr(absent)} }`, ty: STR };
|
|
139
|
+
}
|
|
140
|
+
export function typeofTest(s, t, n) {
|
|
141
|
+
const v = expr(s, t.target);
|
|
142
|
+
const { present, absent } = typeNames(s, v, t.target);
|
|
143
|
+
const konst = (is) => ({ code: String(is === t.equal), ty: BOOL, konst: is === t.equal });
|
|
144
|
+
if (absent === null)
|
|
145
|
+
return konst(present === t.literal);
|
|
146
|
+
if (absent === "either" && (t.literal === "undefined" || t.literal === "object"))
|
|
147
|
+
return eitherRefused(s, t.literal, n);
|
|
148
|
+
if (present === t.literal && absent === t.literal)
|
|
149
|
+
return konst(true);
|
|
150
|
+
if (present === t.literal)
|
|
151
|
+
return boolOf(t.equal ? `${atom(v.code)}.is_some()` : `${atom(v.code)}.is_none()`);
|
|
152
|
+
if (absent === t.literal)
|
|
153
|
+
return boolOf(t.equal ? `${atom(v.code)}.is_none()` : `${atom(v.code)}.is_some()`);
|
|
154
|
+
return konst(false);
|
|
155
|
+
}
|
|
86
156
|
export function presence(s, n) {
|
|
87
157
|
for (const p of ctx.plugins) {
|
|
88
158
|
const own = p.presence?.(s, n);
|
|
@@ -97,12 +167,19 @@ export function presence(s, n) {
|
|
|
97
167
|
negated = true;
|
|
98
168
|
}
|
|
99
169
|
else if (n.type === "BinaryExpression") {
|
|
170
|
+
const tested = typeofCompare(n);
|
|
100
171
|
const test = nullTest(n);
|
|
101
|
-
if (
|
|
172
|
+
if (tested) {
|
|
173
|
+
target = tested.target;
|
|
174
|
+
byTruth = false;
|
|
175
|
+
}
|
|
176
|
+
else if (!test)
|
|
102
177
|
return null;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
178
|
+
else {
|
|
179
|
+
target = test.target;
|
|
180
|
+
negated = test.is;
|
|
181
|
+
byTruth = false;
|
|
182
|
+
}
|
|
106
183
|
}
|
|
107
184
|
const path = pathOf(target);
|
|
108
185
|
if (path === null)
|
|
@@ -110,7 +187,19 @@ export function presence(s, n) {
|
|
|
110
187
|
const v = expr(s, target);
|
|
111
188
|
if (v.ty.k !== "opt")
|
|
112
189
|
return null;
|
|
113
|
-
|
|
190
|
+
const tested = typeofCompare(n);
|
|
191
|
+
if (tested) {
|
|
192
|
+
const { present, absent } = typeNames(s, v, target);
|
|
193
|
+
if (absent === "either" || present === absent)
|
|
194
|
+
return null;
|
|
195
|
+
if (tested.literal === present)
|
|
196
|
+
negated = !tested.equal;
|
|
197
|
+
else if (tested.literal === absent)
|
|
198
|
+
negated = tested.equal;
|
|
199
|
+
else
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
else if (n.type === "BinaryExpression")
|
|
114
203
|
checkNullTest(s, nullTest(n), v);
|
|
115
204
|
const of = v.ty.of;
|
|
116
205
|
const scalar = of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool";
|
package/dist/numbers.d.ts
CHANGED
|
@@ -6,3 +6,7 @@ export declare function numberVal(x: number, int: boolean): Val;
|
|
|
6
6
|
export declare function arithmetic(a: Val, op: string, b: Val, int: boolean): Val;
|
|
7
7
|
export declare function compare(a: Val, op: string, b: Val): Val;
|
|
8
8
|
export declare function negatedOrder(s: Scope, n: N): Val | null;
|
|
9
|
+
/** `a ** b` where every engine gives the same answer: an integer to a whole power from 0 whose
|
|
10
|
+
* result is an integer within ±2⁵³, worked out exactly; `null` for any other. */
|
|
11
|
+
export declare function exactPower(a: number, b: number): number | null;
|
|
12
|
+
export declare function power(s: Scope, base: N, exponent: N, n: N, what: string): Val;
|
package/dist/numbers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BOOL, FLOAT, INT } from "./model.js";
|
|
2
|
-
import { AS, binary, FLIPPED, operand, UNARY } from "./parens.js";
|
|
1
|
+
import { BOOL, fail, FLOAT, INT } from "./model.js";
|
|
2
|
+
import { AS, atom, binary, FLIPPED, operand, UNARY } from "./parens.js";
|
|
3
3
|
import { expr } from "./expr.js";
|
|
4
4
|
export function isNumber(ty) {
|
|
5
5
|
return ty.k === "int" || ty.k === "float";
|
|
@@ -20,7 +20,31 @@ function floatLiteral(x) {
|
|
|
20
20
|
const spelled = String(Math.abs(x)).replace("e+", "e");
|
|
21
21
|
return `${x < 0 || Object.is(x, -0) ? "-" : ""}${/[.e]/.test(spelled) ? spelled : `${spelled}.0`}f64`;
|
|
22
22
|
}
|
|
23
|
+
const RUST_CONSTANTS = new Map(Object.entries({
|
|
24
|
+
E: "2.71828182845904523536028747135266250",
|
|
25
|
+
FRAC_1_PI: "0.318309886183790671537767526745028724",
|
|
26
|
+
FRAC_1_SQRT_2: "0.707106781186547524400844362104849039",
|
|
27
|
+
FRAC_2_PI: "0.636619772367581343075535053490057448",
|
|
28
|
+
FRAC_2_SQRT_PI: "1.12837916709551257389615890312154517",
|
|
29
|
+
FRAC_PI_2: "1.57079632679489661923132169163975144",
|
|
30
|
+
FRAC_PI_3: "1.04719755119659774615421446109316763",
|
|
31
|
+
FRAC_PI_4: "0.785398163397448309615660845819875721",
|
|
32
|
+
FRAC_PI_6: "0.52359877559829887307710723054658381",
|
|
33
|
+
FRAC_PI_8: "0.39269908169872415480783042290993786",
|
|
34
|
+
LN_10: "2.30258509299404568401799145468436421",
|
|
35
|
+
LN_2: "0.693147180559945309417232121458176568",
|
|
36
|
+
LOG10_2: "0.301029995663981195213738894724493027",
|
|
37
|
+
LOG10_E: "0.434294481903251827651128918916605082",
|
|
38
|
+
LOG2_10: "3.32192809488736234787031942948939018",
|
|
39
|
+
LOG2_E: "1.44269504088896340735992468100189214",
|
|
40
|
+
PI: "3.14159265358979323846264338327950288",
|
|
41
|
+
SQRT_2: "1.41421356237309504880168872420969808",
|
|
42
|
+
TAU: "6.28318530717958647692528676655900577",
|
|
43
|
+
}).map(([name, digits]) => [Number(digits), `std::f64::consts::${name}`]));
|
|
23
44
|
export function numberVal(x, int) {
|
|
45
|
+
const constant = int ? undefined : RUST_CONSTANTS.get(Math.abs(x));
|
|
46
|
+
if (constant !== undefined)
|
|
47
|
+
return { code: x < 0 ? `-${constant}` : constant, ty: FLOAT, num: x };
|
|
24
48
|
if (!int)
|
|
25
49
|
return { code: floatLiteral(x), ty: FLOAT, num: x };
|
|
26
50
|
if (!Number.isSafeInteger(x))
|
|
@@ -63,6 +87,13 @@ export function compare(a, op, b) {
|
|
|
63
87
|
const konst = fold(a.num, op, b.num);
|
|
64
88
|
return { code: String(konst), ty: BOOL, konst };
|
|
65
89
|
}
|
|
90
|
+
if (asF64(a) === asF64(b)) {
|
|
91
|
+
if (op === "<" || op === ">")
|
|
92
|
+
return { code: "false", ty: BOOL, konst: false };
|
|
93
|
+
if (a.ty.k === "int")
|
|
94
|
+
return { code: "true", ty: BOOL, konst: true };
|
|
95
|
+
return { code: `!${atom(asF64(a))}.is_nan()`, ty: BOOL };
|
|
96
|
+
}
|
|
66
97
|
return { code: binary(asF64(a), op, asF64(b)), ty: BOOL };
|
|
67
98
|
}
|
|
68
99
|
export function negatedOrder(s, n) {
|
|
@@ -72,3 +103,31 @@ export function negatedOrder(s, n) {
|
|
|
72
103
|
const b = expr(s, n.right);
|
|
73
104
|
return a.ty.k === "int" && b.ty.k === "int" ? compare(a, FLIPPED[n.operator], b) : null;
|
|
74
105
|
}
|
|
106
|
+
/** `a ** b` where every engine gives the same answer: an integer to a whole power from 0 whose
|
|
107
|
+
* result is an integer within ±2⁵³, worked out exactly; `null` for any other. */
|
|
108
|
+
export function exactPower(a, b) {
|
|
109
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || b < 0 || Math.abs(a) > 2 ** 53)
|
|
110
|
+
return null;
|
|
111
|
+
if (b === 0)
|
|
112
|
+
return 1;
|
|
113
|
+
if (Object.is(a, -0))
|
|
114
|
+
return b % 2 === 1 ? -0 : 0;
|
|
115
|
+
if (Math.abs(a) > 1 && b > 53)
|
|
116
|
+
return null;
|
|
117
|
+
const r = BigInt(a) ** BigInt(b);
|
|
118
|
+
const limit = 2n ** 53n;
|
|
119
|
+
return r > limit || r < -limit ? null : Number(r);
|
|
120
|
+
}
|
|
121
|
+
export function power(s, base, exponent, n, what) {
|
|
122
|
+
const [a, b] = [expr(s, base), expr(s, exponent)];
|
|
123
|
+
if (!isNumber(a.ty) || !isNumber(b.ty))
|
|
124
|
+
return fail(s.comp, "FV0720", `${what} of numbers that are present`, n);
|
|
125
|
+
const engines = "Node 22 writes `10 ** -20` as `1.0000000000000001e-20`, and engines round powers beyond 2⁵³ in their own ways";
|
|
126
|
+
if (a.num === undefined || b.num === undefined) {
|
|
127
|
+
return fail(s.comp, "FV0720", `${what} of a value known only when rendering: ferrovue works ${what} out when compiling, for an integer to a whole power from 0 whose result is within ±2⁵³, since ${engines}: compute it in Rust and pass it as a prop`, n);
|
|
128
|
+
}
|
|
129
|
+
const r = exactPower(a.num, b.num);
|
|
130
|
+
if (r === null)
|
|
131
|
+
return fail(s.comp, "FV0720", `${what} of ${String(a.num)} and ${String(b.num)}, which is not an integer to a whole power from 0 with a result within ±2⁵³: ${engines}`, n);
|
|
132
|
+
return numberVal(r, true);
|
|
133
|
+
}
|
package/dist/parens.js
CHANGED
|
@@ -178,7 +178,14 @@ export function strArg(code) {
|
|
|
178
178
|
const branches = ifElse(code);
|
|
179
179
|
if (branches)
|
|
180
180
|
return `if ${branches.test} { ${strArg(branches.yes)} } else { ${strArg(branches.no)} }`;
|
|
181
|
+
if (/^\(\*\w+\)$/.test(code))
|
|
182
|
+
return bare(code);
|
|
181
183
|
const m = /^&\*(\**)(.*)$/s.exec(code);
|
|
184
|
+
if (m && !m[1]) {
|
|
185
|
+
const lazy = /^\(\*(\w+)\)$/.exec(m[2])?.[1];
|
|
186
|
+
if (lazy !== undefined)
|
|
187
|
+
return `&${lazy}`;
|
|
188
|
+
}
|
|
182
189
|
if (!m || (binding(m[2]) < ATOM && !enclosed(m[2])))
|
|
183
190
|
return code;
|
|
184
191
|
return m[1] ? m[2] : `&${m[2]}`;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface ValueHooks {
|
|
|
34
34
|
} | undefined;
|
|
35
35
|
nullish?(s: Scope, a: Val, b: Val, n: N): Val | undefined;
|
|
36
36
|
equals?(a: Val, b: Val): string | undefined;
|
|
37
|
+
order?(a: Val, op: string, b: Val): Val | undefined;
|
|
38
|
+
refuseEquality?(s: Scope, a: Val, b: Val, n: N): void;
|
|
39
|
+
json?(v: Val): string | undefined;
|
|
37
40
|
isArray?(v: Val): Val | undefined;
|
|
38
41
|
}
|
|
39
42
|
export interface ReadComponent {
|
|
@@ -75,6 +78,8 @@ export interface Plugin<Run = unknown, Local = unknown> {
|
|
|
75
78
|
};
|
|
76
79
|
global?(s: Scope, name: string, n: N): Val | null;
|
|
77
80
|
call?(s: Scope, n: N): Val | null;
|
|
81
|
+
construct?(s: Scope, n: N): Val | null;
|
|
82
|
+
method?(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
|
|
78
83
|
member?(s: Scope, base: Val, prop: string, n: N, computed: boolean): Val | null;
|
|
79
84
|
equality?(s: Scope, n: N): Val | null;
|
|
80
85
|
presence?(s: Scope, n: N): Presence | null | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type N, type Scope } from "../model.ts";
|
|
2
|
+
import { type Plugin } from "../plugin.ts";
|
|
3
|
+
declare module "../model.ts" {
|
|
4
|
+
interface PluginTys {
|
|
5
|
+
date: {
|
|
6
|
+
k: "date";
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** The time value `Date.parse(s)` gives a string every JavaScript engine reads alike: one in
|
|
11
|
+
* ECMAScript's date time string format, a date alone or a date and time with a `Z` or an offset, its
|
|
12
|
+
* fields within their ranges. `undefined` for any other string, which an engine may read by rules of
|
|
13
|
+
* its own, or in the time zone of the machine running it, and for a date and time whose fields lie
|
|
14
|
+
* beyond ±8.64 × 10¹⁵ ms read as UTC while its offset brings it within, which Safari reads as an
|
|
15
|
+
* invalid date and the other engines as a valid one. */
|
|
16
|
+
export declare function portableTime(s: string): number | undefined;
|
|
17
|
+
/** Whether `Date.UTC(...parts)` gives what every JavaScript engine gives. V8 reads a year beyond
|
|
18
|
+
* ±1,000,000, a month beyond ±10,000,000, and a year before −399,999 once the month is carried into
|
|
19
|
+
* it by rules of its own, which Firefox and Safari do not share, so those are portable only where
|
|
20
|
+
* the date is invalid in every engine; and a part worth more than 2⁵³ ms that the others bring back
|
|
21
|
+
* within the range of dates is rounded by Node otherwise than by the browsers. */
|
|
22
|
+
export declare function portableUtc(parts: readonly number[]): boolean;
|
|
23
|
+
/** The refusal of anything that formats by locale, which `Intl` decides from data each engine ships. */
|
|
24
|
+
export declare function localeRefusal(s: Scope, what: string, n: N): never;
|
|
25
|
+
export declare const dates: Plugin;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { fail, FLOAT, nothing, STR } from "../model.js";
|
|
2
|
+
import {} from "../plugin.js";
|
|
3
|
+
import { bare, strArg } from "../parens.js";
|
|
4
|
+
import { asF64, compare, numberVal } from "../numbers.js";
|
|
5
|
+
import { describeTy, expr } from "../expr.js";
|
|
6
|
+
const GUIDE = "see the `dates_and_prices` guide";
|
|
7
|
+
const PORTABLE = /^([+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{3})?)?(Z|[+-](\d{2}):(\d{2}))?)?$/;
|
|
8
|
+
function daysInMonth(year, month) {
|
|
9
|
+
if (month === 2)
|
|
10
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
11
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
12
|
+
}
|
|
13
|
+
/** The time value `Date.parse(s)` gives a string every JavaScript engine reads alike: one in
|
|
14
|
+
* ECMAScript's date time string format, a date alone or a date and time with a `Z` or an offset, its
|
|
15
|
+
* fields within their ranges. `undefined` for any other string, which an engine may read by rules of
|
|
16
|
+
* its own, or in the time zone of the machine running it, and for a date and time whose fields lie
|
|
17
|
+
* beyond ±8.64 × 10¹⁵ ms read as UTC while its offset brings it within, which Safari reads as an
|
|
18
|
+
* invalid date and the other engines as a valid one. */
|
|
19
|
+
export function portableTime(s) {
|
|
20
|
+
const m = PORTABLE.exec(s);
|
|
21
|
+
if (!m || m[1] === "-000000" || (m[4] !== undefined && m[7] === undefined))
|
|
22
|
+
return undefined;
|
|
23
|
+
const [year, month, day, hours, minutes, seconds, , zoneHours, zoneMinutes] = m.slice(1).map((x) => (x === undefined ? undefined : Number(x)));
|
|
24
|
+
const within = (x, low, high) => x === undefined || (x >= low && x <= high);
|
|
25
|
+
const fields = [within(month, 1, 12), within(day, 1, daysInMonth(year, month ?? 1)), within(hours, 0, 23), within(minutes, 0, 59), within(seconds, 0, 59), within(zoneHours, 0, 23), within(zoneMinutes, 0, 59)];
|
|
26
|
+
if (!fields.every(Boolean))
|
|
27
|
+
return undefined;
|
|
28
|
+
const t = Date.parse(s);
|
|
29
|
+
const zone = m[7];
|
|
30
|
+
if (zone !== undefined && zone !== "Z" && Number.isNaN(Date.parse(s.slice(0, -zone.length) + "Z")) && !Number.isNaN(t))
|
|
31
|
+
return undefined;
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
34
|
+
const COMMON_DAYS = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
|
35
|
+
function v8Day(year, month, date) {
|
|
36
|
+
const before = (y) => 365 * (y + 399_999) + Math.trunc((y + 399_999) / 4) - Math.trunc((y + 399_999) / 100) + Math.trunc((y + 399_999) / 400);
|
|
37
|
+
let y = Math.trunc(year) + Math.trunc(Math.trunc(month) / 12);
|
|
38
|
+
let m = Math.trunc(month) % 12;
|
|
39
|
+
if (m < 0)
|
|
40
|
+
[m, y] = [m + 12, y - 1];
|
|
41
|
+
const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
|
|
42
|
+
return before(y) - before(1970) + COMMON_DAYS[m] + (leap && m >= 2 ? 1 : 0) - 1 + Math.trunc(date);
|
|
43
|
+
}
|
|
44
|
+
/** Whether `Date.UTC(...parts)` gives what every JavaScript engine gives. V8 reads a year beyond
|
|
45
|
+
* ±1,000,000, a month beyond ±10,000,000, and a year before −399,999 once the month is carried into
|
|
46
|
+
* it by rules of its own, which Firefox and Safari do not share, so those are portable only where
|
|
47
|
+
* the date is invalid in every engine; and a part worth more than 2⁵³ ms that the others bring back
|
|
48
|
+
* within the range of dates is rounded by Node otherwise than by the browsers. */
|
|
49
|
+
export function portableUtc(parts) {
|
|
50
|
+
const [year = NaN, month = 0, date = 1, hours = 0, minutes = 0, seconds = 0, ms = 0] = parts;
|
|
51
|
+
if (![year, month, date, hours, minutes, seconds, ms].every(Number.isFinite))
|
|
52
|
+
return true;
|
|
53
|
+
const int = Math.trunc;
|
|
54
|
+
const terms = [int(hours) * 3_600_000, int(minutes) * 60_000, int(seconds) * 1000];
|
|
55
|
+
const time = terms[0] + terms[1] + terms[2] + int(ms);
|
|
56
|
+
if (!Number.isFinite(time))
|
|
57
|
+
return true;
|
|
58
|
+
const y = int(year) >= 0 && int(year) <= 99 ? 1900 + int(year) : year;
|
|
59
|
+
const carried = int(y) + Math.floor(int(month) / 12);
|
|
60
|
+
if (!(Math.abs(y) <= 1e6 && Math.abs(month) <= 1e7 && carried >= -399_999)) {
|
|
61
|
+
return Math.abs(carried) >= 399_999 && Math.abs(int(date)) <= 1e7 && Math.abs(time) <= 1e15;
|
|
62
|
+
}
|
|
63
|
+
const days = v8Day(y, month, date) * 86_400_000;
|
|
64
|
+
const t = days + time;
|
|
65
|
+
const largest = Math.max(Math.abs(days), ...terms.map(Math.abs));
|
|
66
|
+
return !(Number.isFinite(t) && largest > 2 ** 53 && Math.abs(t) <= 8.64e15 + largest * 2 ** -50);
|
|
67
|
+
}
|
|
68
|
+
const UTC_DEFAULTS = ["0.0f64", "1.0f64", "0.0f64", "0.0f64", "0.0f64", "0.0f64"];
|
|
69
|
+
function fromParts(s, n) {
|
|
70
|
+
const args = n.arguments;
|
|
71
|
+
if (args.length < 1 || args.length > 7)
|
|
72
|
+
return fail(s.comp, "FV1807", `\`Date.UTC()\` takes from one to seven numbers: a year, a month from 0 and the day, hours, minutes, seconds and milliseconds that follow (${GUIDE})`, n);
|
|
73
|
+
const vals = args.map((a) => {
|
|
74
|
+
const v = expr(s, a);
|
|
75
|
+
if (v.ty.k !== "int" && v.ty.k !== "float")
|
|
76
|
+
fail(s.comp, "FV1807", `\`Date.UTC()\` takes numbers that are present: this is ${describeTy(v.ty)}`, a);
|
|
77
|
+
return v;
|
|
78
|
+
});
|
|
79
|
+
if (vals.every((v) => v.num !== undefined)) {
|
|
80
|
+
const parts = vals.map((v) => v.num);
|
|
81
|
+
if (!portableUtc(parts)) {
|
|
82
|
+
return fail(s.comp, "FV1808", `\`Date.UTC(${parts.join(", ")})\` is a date engines compute differently: V8 reads a year beyond ±1,000,000, a month beyond ±10,000,000 and a year before −399,999 by rules of its own, and Node rounds a part beyond 2⁵³ ms otherwise than the browsers (${GUIDE})`, n);
|
|
83
|
+
}
|
|
84
|
+
return numberVal(Date.UTC(...parts), false);
|
|
85
|
+
}
|
|
86
|
+
const code = [...vals.map((v) => bare(asF64(v))), ...UTC_DEFAULTS.slice(vals.length - 1)].join(", ");
|
|
87
|
+
return { code: `fv::js_date_utc(${code})`, ty: FLOAT };
|
|
88
|
+
}
|
|
89
|
+
const LOCAL_GETTERS = new Map([
|
|
90
|
+
["getFullYear", "getUTCFullYear"],
|
|
91
|
+
["getYear", "getUTCFullYear"],
|
|
92
|
+
["getMonth", "getUTCMonth"],
|
|
93
|
+
["getDate", "getUTCDate"],
|
|
94
|
+
["getDay", "getUTCDay"],
|
|
95
|
+
["getHours", "getUTCHours"],
|
|
96
|
+
["getMinutes", "getUTCMinutes"],
|
|
97
|
+
["getSeconds", "getUTCSeconds"],
|
|
98
|
+
["getMilliseconds", "getUTCMilliseconds"],
|
|
99
|
+
]);
|
|
100
|
+
const UTC_GETTERS = new Map([
|
|
101
|
+
["getUTCFullYear", "full_year"],
|
|
102
|
+
["getUTCMonth", "month"],
|
|
103
|
+
["getUTCDate", "date"],
|
|
104
|
+
["getUTCDay", "day"],
|
|
105
|
+
["getUTCHours", "hours"],
|
|
106
|
+
["getUTCMinutes", "minutes"],
|
|
107
|
+
["getUTCSeconds", "seconds"],
|
|
108
|
+
["getUTCMilliseconds", "milliseconds"],
|
|
109
|
+
]);
|
|
110
|
+
const LOCALE_METHODS = new Set(["toLocaleString", "toLocaleDateString", "toLocaleTimeString", "localeCompare"]);
|
|
111
|
+
const DATE = { k: "date" };
|
|
112
|
+
function global(s, n, name) {
|
|
113
|
+
return n?.type === "Identifier" && n.name === name && !s.locals.has(name) && !s.setup.has(name);
|
|
114
|
+
}
|
|
115
|
+
function rootOf(n) {
|
|
116
|
+
while (n?.type === "MemberExpression" || n?.type === "CallExpression" || n?.type === "NewExpression")
|
|
117
|
+
n = n.type === "MemberExpression" ? n.object : n.callee;
|
|
118
|
+
return n;
|
|
119
|
+
}
|
|
120
|
+
/** The refusal of anything that formats by locale, which `Intl` decides from data each engine ships. */
|
|
121
|
+
export function localeRefusal(s, what, n) {
|
|
122
|
+
return fail(s.comp, "FV1805", `${what} formats by locale with \`Intl\`, whose output differs between Node, Chromium, Firefox and Safari: format the value in Rust and pass a string, or translate it with \`$t\` (${GUIDE})`, n);
|
|
123
|
+
}
|
|
124
|
+
function parsed(s, arg, what) {
|
|
125
|
+
if (arg.type === "StringLiteral") {
|
|
126
|
+
const t = portableTime(arg.value);
|
|
127
|
+
if (t === undefined) {
|
|
128
|
+
fail(s.comp, "FV1803", `${what} of ${JSON.stringify(arg.value)}, which is not a date alone nor a date and time with a \`Z\` or an offset in ECMAScript's date time string format, so each browser may read it differently (${GUIDE})`, arg);
|
|
129
|
+
}
|
|
130
|
+
return numberVal(t, false);
|
|
131
|
+
}
|
|
132
|
+
const v = expr(s, arg);
|
|
133
|
+
if (v.ty.k !== "str")
|
|
134
|
+
return fail(s.comp, "FV1802", `${what} takes a string that is present`, arg);
|
|
135
|
+
return { code: `fv::js_date_parse(${strArg(v.code)})`, ty: FLOAT };
|
|
136
|
+
}
|
|
137
|
+
function constructed(s, n) {
|
|
138
|
+
const args = n.arguments;
|
|
139
|
+
if (args.length === 0)
|
|
140
|
+
return fail(s.comp, "FV1801", `\`new Date()\` is the time it runs, which differs between the server's render and the browser's hydration: pass the time as a prop (${GUIDE})`, n);
|
|
141
|
+
if (args.length > 1) {
|
|
142
|
+
return fail(s.comp, "FV1802", `\`new Date(year, month, …)\` reads its parts in the time zone of the machine running it: pass an ISO string with a \`Z\` or an offset, or a number of milliseconds (${GUIDE})`, n);
|
|
143
|
+
}
|
|
144
|
+
if (args[0].type === "StringLiteral")
|
|
145
|
+
return { code: parsed(s, args[0], "`new Date()`").code, ty: DATE };
|
|
146
|
+
const v = expr(s, args[0]);
|
|
147
|
+
if (v.ty.k === "date")
|
|
148
|
+
return v;
|
|
149
|
+
if (v.ty.k === "str")
|
|
150
|
+
return { code: `fv::js_date_parse(${strArg(v.code)})`, ty: DATE };
|
|
151
|
+
if (v.ty.k === "int" || v.ty.k === "float")
|
|
152
|
+
return { code: `fv::js_time_clip(${bare(asF64(v))})`, ty: DATE };
|
|
153
|
+
return fail(s.comp, "FV1802", `\`new Date()\` takes one string or one number that is present (${GUIDE})`, args[0]);
|
|
154
|
+
}
|
|
155
|
+
export const dates = {
|
|
156
|
+
name: "dates",
|
|
157
|
+
construct(s, n) {
|
|
158
|
+
if (global(s, rootOf(n.callee), "Intl"))
|
|
159
|
+
return localeRefusal(s, "`Intl`", n);
|
|
160
|
+
return global(s, n.callee, "Date") ? constructed(s, n) : null;
|
|
161
|
+
},
|
|
162
|
+
call(s, n) {
|
|
163
|
+
const callee = n.callee;
|
|
164
|
+
if (callee.type !== "MemberExpression" || callee.computed)
|
|
165
|
+
return null;
|
|
166
|
+
const method = callee.property.name;
|
|
167
|
+
const object = callee.object;
|
|
168
|
+
if (object.type === "Identifier" && object.name === "_ctx" && (method === "$d" || method === "$n"))
|
|
169
|
+
return localeRefusal(s, `vue-i18n's \`${method}\``, n);
|
|
170
|
+
const own = object.type === "Identifier" && (object.name === "_ctx" || object.name === "$setup");
|
|
171
|
+
if (!own && LOCALE_METHODS.has(method))
|
|
172
|
+
return localeRefusal(s, `\`.${method}()\``, n);
|
|
173
|
+
if (!global(s, object, "Date"))
|
|
174
|
+
return null;
|
|
175
|
+
if (method === "now")
|
|
176
|
+
return fail(s.comp, "FV1801", `\`Date.now()\` is the time it runs, which differs between the server's render and the browser's hydration: pass the time as a prop (${GUIDE})`, n);
|
|
177
|
+
if (method === "parse" && n.arguments.length === 1)
|
|
178
|
+
return parsed(s, n.arguments[0], "`Date.parse()`");
|
|
179
|
+
if (method === "UTC")
|
|
180
|
+
return fromParts(s, n);
|
|
181
|
+
return fail(s.comp, "FV1804", `\`Date.${method}()\` is not supported: build a date with \`new Date()\` of an ISO string or a number of milliseconds, or with \`Date.UTC()\` of its parts (${GUIDE})`, n);
|
|
182
|
+
},
|
|
183
|
+
method(s, target, method, args, n) {
|
|
184
|
+
if (target.ty.k !== "date")
|
|
185
|
+
return null;
|
|
186
|
+
const t = bare(target.code);
|
|
187
|
+
const plain = args.length === 0;
|
|
188
|
+
if (plain && method === "toISOString")
|
|
189
|
+
return { code: `&*fv::js_to_iso_string(${t})`, ty: STR };
|
|
190
|
+
if (plain && method === "toUTCString")
|
|
191
|
+
return { code: `&*fv::js_to_utc_string(${t})`, ty: STR };
|
|
192
|
+
if (plain && method === "toJSON")
|
|
193
|
+
return { code: `fv::js_to_json(${t}).as_deref()`, ty: { k: "opt", of: STR, none: "null" }, held: `fv::js_to_json(${t}).map(std::borrow::Cow::<str>::Owned)` };
|
|
194
|
+
if (plain && (method === "getTime" || method === "valueOf"))
|
|
195
|
+
return { code: target.code, ty: FLOAT };
|
|
196
|
+
const utc = UTC_GETTERS.get(method);
|
|
197
|
+
if (plain && utc !== undefined)
|
|
198
|
+
return { code: `fv::js_utc_${utc}(${t})`, ty: FLOAT };
|
|
199
|
+
if (LOCAL_GETTERS.has(method) || method === "getTimezoneOffset") {
|
|
200
|
+
const instead = LOCAL_GETTERS.get(method);
|
|
201
|
+
return fail(s.comp, "FV1804", `\`.${method}()\` reads the date in the time zone of the machine running it, which differs between the server and the reader's browser${instead ? `: use \`.${instead}()\`` : ""} (${GUIDE})`, n);
|
|
202
|
+
}
|
|
203
|
+
return fail(s.comp, "FV1804", `\`.${method}(${plain ? "" : "…"})\` on a \`Date\` is not supported: a date is read with \`.toISOString()\`, \`.toUTCString()\`, \`.toJSON()\`, \`.getTime()\` and the \`.getUTC…()\` methods (${GUIDE})`, n);
|
|
204
|
+
},
|
|
205
|
+
values: {
|
|
206
|
+
order(a, op, b) {
|
|
207
|
+
if (a.ty.k !== "date" || b.ty.k !== "date")
|
|
208
|
+
return undefined;
|
|
209
|
+
return compare({ code: a.code, ty: FLOAT }, op, { code: b.code, ty: FLOAT });
|
|
210
|
+
},
|
|
211
|
+
refuseEquality(s, a, b, n) {
|
|
212
|
+
const dated = (ty) => ty.k === "date" || (ty.k === "opt" && ty.of.k === "date");
|
|
213
|
+
if (!(dated(a.ty) || dated(b.ty)) || nothing(a.ty) || nothing(b.ty))
|
|
214
|
+
return;
|
|
215
|
+
const why = dated(a.ty) && dated(b.ty)
|
|
216
|
+
? "is true only when the two are one object, so two dates of the same time are unequal"
|
|
217
|
+
: "compares the date as an object, or as its `.toString()` in the time zone of the machine running it";
|
|
218
|
+
fail(s.comp, "FV1809", `\`${n.operator}\` between ${describeTy(a.ty)} and ${describeTy(b.ty)} ${why}: compare time values, \`a.getTime() === b.getTime()\` (${GUIDE})`, n);
|
|
219
|
+
},
|
|
220
|
+
json: (v) => (v.ty.k === "date" ? `fv::js_json_date(${bare(v.code)})` : undefined),
|
|
221
|
+
describe: (ty) => (ty.k === "date" ? "a `Date`" : undefined),
|
|
222
|
+
unbindable: (ty) => ty.k === "date" ? { what: "a `Date`", fix: "bind `.toISOString()`, or format it in Rust and pass a string" } : undefined,
|
|
223
|
+
},
|
|
224
|
+
};
|
package/dist/plugins/i18n.js
CHANGED
|
@@ -67,7 +67,7 @@ function messageSource(file, key, ast) {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
const items = (c) => c.items ?? (c.static !== undefined ? [{ type: 3, value: c.static }] : []);
|
|
70
|
-
return `Message
|
|
70
|
+
return `Message::new(&[${cases.map((c) => `&[${items(c).map(part).join(", ")}]`).join(", ")}])`;
|
|
71
71
|
}
|
|
72
72
|
function byteOrder(a, b) {
|
|
73
73
|
return Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
@@ -76,13 +76,12 @@ function i18nSource(setup) {
|
|
|
76
76
|
const locales = setup.locales
|
|
77
77
|
.map((l) => {
|
|
78
78
|
const keys = [...l.messages.keys()].toSorted(byteOrder);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
if (keys.length === 0)
|
|
80
|
+
return ` Locale::new(${rustStr(l.name)}, &[]),`;
|
|
81
|
+
const entries = keys.map((k) => ` (${rustStr(k)}, ${messageSource(l.file, k, l.messages.get(k))}),`).join("\n");
|
|
82
|
+
return ` Locale::new(${rustStr(l.name)}, &[
|
|
83
83
|
${entries}
|
|
84
|
-
|
|
85
|
-
},`;
|
|
84
|
+
]),`;
|
|
86
85
|
})
|
|
87
86
|
.join("\n");
|
|
88
87
|
return `${header(setup.dir.replace(/\/?$/, "/"), "the locale files")}
|
|
@@ -130,9 +129,9 @@ function translate(s, args, n) {
|
|
|
130
129
|
const key = expr(s, args[0]);
|
|
131
130
|
if (key.ty.k !== "str")
|
|
132
131
|
fail(s.comp, "FV1409", "the key given to `t()` is a string", args[0]);
|
|
133
|
-
let named = "
|
|
134
|
-
let list = "
|
|
135
|
-
let plural = "
|
|
132
|
+
let named = "";
|
|
133
|
+
let list = "";
|
|
134
|
+
let plural = "";
|
|
136
135
|
for (const a of args.slice(1)) {
|
|
137
136
|
if (a.type === "ObjectExpression") {
|
|
138
137
|
const pairs = a.properties.map((p) => {
|
|
@@ -140,10 +139,10 @@ function translate(s, args, n) {
|
|
|
140
139
|
fail(s.comp, "FV1410", "named values hold plain keys", p);
|
|
141
140
|
return `(${rustStr(String(p.key.name ?? p.key.value))}, ${i18nValue(s, p.value)})`;
|
|
142
141
|
});
|
|
143
|
-
named =
|
|
142
|
+
named = `.with_named(&[${pairs.join(", ")}])`;
|
|
144
143
|
}
|
|
145
144
|
else if (a.type === "ArrayExpression") {
|
|
146
|
-
list =
|
|
145
|
+
list = `.with_list(&[${a.elements.map((el) => i18nValue(s, el)).join(", ")}])`;
|
|
147
146
|
}
|
|
148
147
|
else {
|
|
149
148
|
const v = expr(s, a);
|
|
@@ -151,10 +150,10 @@ function translate(s, args, n) {
|
|
|
151
150
|
fail(s.comp, "FV1411", "a default message given to `t()` is not supported: add the message to the locale files", a);
|
|
152
151
|
if (v.ty.k !== "int")
|
|
153
152
|
fail(s.comp, "FV1412", "the plural number given to `t()` is an integer", a);
|
|
154
|
-
plural =
|
|
153
|
+
plural = `.with_plural(${bare(v.code)})`;
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
|
-
return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args
|
|
156
|
+
return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args::new()${named}${list}${plural})`, ty: STR };
|
|
158
157
|
}
|
|
159
158
|
export const i18n = {
|
|
160
159
|
name: "i18n",
|
|
@@ -193,6 +192,9 @@ export const i18n = {
|
|
|
193
192
|
s.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
|
|
194
193
|
s.refs.add(p.value.name);
|
|
195
194
|
}
|
|
195
|
+
else if (key === "d" || key === "n") {
|
|
196
|
+
s.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` formats by locale with \`Intl\`, whose output differs between Node and each browser (see the \`dates_and_prices\` guide)`);
|
|
197
|
+
}
|
|
196
198
|
else
|
|
197
199
|
s.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
|
|
198
200
|
}
|
package/dist/plugins/index.js
CHANGED
|
@@ -10,4 +10,5 @@ import { twinsPlugin } from "./twins.js";
|
|
|
10
10
|
import { provideInject } from "./provide.js";
|
|
11
11
|
import { head } from "./head.js";
|
|
12
12
|
import { cssModules } from "./css-modules.js";
|
|
13
|
-
|
|
13
|
+
import { dates } from "./dates.js";
|
|
14
|
+
export const PLUGINS = [router, piniaStores, sharedTypes, i18n, teleport, scoped, clientOnly, twinsPlugin, provideInject, head, cssModules, dates];
|
package/dist/plugins/router.js
CHANGED
|
@@ -109,9 +109,10 @@ function routeDefs(routes, depth) {
|
|
|
109
109
|
const pad = " ".repeat(depth);
|
|
110
110
|
return routes
|
|
111
111
|
.map((r) => {
|
|
112
|
-
const name = r.name === undefined ? "
|
|
113
|
-
const
|
|
114
|
-
|
|
112
|
+
const name = r.name === undefined ? "" : `.with_name(${rustStr(r.name)})`;
|
|
113
|
+
const view = r.view === false ? ".with_view(false)" : "";
|
|
114
|
+
const children = r.children?.length ? `.with_children(&[\n${routeDefs(r.children, depth + 1)}\n${pad}])` : "";
|
|
115
|
+
return `${pad}ferrovue::RouteDef::new(${rustStr(r.path)})${name}${view}${children},`;
|
|
115
116
|
})
|
|
116
117
|
.join("\n");
|
|
117
118
|
}
|
|
@@ -206,7 +207,7 @@ export const router = {
|
|
|
206
207
|
return null;
|
|
207
208
|
const v = expr(s, tested.target);
|
|
208
209
|
if (v.ty.k !== "query")
|
|
209
|
-
|
|
210
|
+
return null;
|
|
210
211
|
const one = `${atom(v.code)}.attr_value().is_some()`;
|
|
211
212
|
return boolOf(tested.negated ? not(one) : one);
|
|
212
213
|
},
|
|
@@ -216,7 +217,9 @@ export const router = {
|
|
|
216
217
|
return undefined;
|
|
217
218
|
const path = pathOf(t.target);
|
|
218
219
|
const v = expr(s, t.target);
|
|
219
|
-
if (
|
|
220
|
+
if (v.ty.k !== "query")
|
|
221
|
+
return undefined;
|
|
222
|
+
if (path === null)
|
|
220
223
|
return null;
|
|
221
224
|
const option = `${atom(v.code)}.attr_value()`;
|
|
222
225
|
return { path, of: STR, negated: t.negated, truthy: false, option, present: `${option}.is_some()`, pattern: (name) => `let Some(${name}) = ${option}` };
|