prosebook 0.2.0 → 0.3.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 +274 -29
- package/dist/bun.d.ts +34 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +98 -0
- package/dist/bun.js.map +1 -0
- package/dist/bundle-DboItAre.js +111 -0
- package/dist/bundle-DboItAre.js.map +1 -0
- package/dist/bundle-KD-DiiFa.d.ts +6 -0
- package/dist/bundle-KD-DiiFa.d.ts.map +1 -0
- package/dist/cli.js +51 -56
- package/dist/cli.js.map +1 -1
- package/dist/compile-xF3F4oIp.d.ts +16 -0
- package/dist/compile-xF3F4oIp.d.ts.map +1 -0
- package/dist/{config-D--RITk2.js → config-BPSmoYel.js} +36 -212
- package/dist/config-BPSmoYel.js.map +1 -0
- package/dist/{config-_4HfnJQu.d.ts → config-DdMkKt_N.d.ts} +61 -2
- package/dist/config-DdMkKt_N.d.ts.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +1 -1
- package/dist/{copy-DXnzXsJa.d.ts → copy-B3V_QRkT.d.ts} +20 -7
- package/dist/copy-B3V_QRkT.d.ts.map +1 -0
- package/dist/copy-Bo_FIoLy.js +154 -0
- package/dist/copy-Bo_FIoLy.js.map +1 -0
- package/dist/endpoints-CnUfCkIk.js +96 -0
- package/dist/endpoints-CnUfCkIk.js.map +1 -0
- package/dist/generate-9pR3i5Bj.js +5173 -0
- package/dist/generate-9pR3i5Bj.js.map +1 -0
- package/dist/helpers-BWGcD0kS.js +96 -0
- package/dist/helpers-BWGcD0kS.js.map +1 -0
- package/dist/icu-Dej0mNsF.js +218 -0
- package/dist/icu-Dej0mNsF.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -188
- package/dist/inspector.d.ts +4 -1
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +7 -2
- package/dist/inspector.js.map +1 -1
- package/dist/locale-DpQA1pqH.js +70 -0
- package/dist/locale-DpQA1pqH.js.map +1 -0
- package/dist/next-loader.d.ts +11 -0
- package/dist/next-loader.d.ts.map +1 -0
- package/dist/next-loader.js +17 -0
- package/dist/next-loader.js.map +1 -0
- package/dist/next.d.ts +30 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +160 -0
- package/dist/next.js.map +1 -0
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +20 -0
- package/dist/react/server.d.ts.map +1 -0
- package/dist/react/server.js +34 -0
- package/dist/react/server.js.map +1 -0
- package/dist/{sarif-CI8HOokm.js → sarif-CWyTAQ0y.js} +103 -87
- package/dist/sarif-CWyTAQ0y.js.map +1 -0
- package/dist/scan-Bx0_yDSi.js +387 -0
- package/dist/scan-Bx0_yDSi.js.map +1 -0
- package/dist/tools.d.ts +83 -90
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -4
- package/dist/vite.d.ts +3 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +29 -109
- package/dist/vite.js.map +1 -1
- package/package.json +37 -4
- package/src/bun.ts +132 -0
- package/src/cli.ts +60 -58
- package/src/index.ts +1 -1
- package/src/inspector.ts +8 -3
- package/src/next-loader.ts +16 -0
- package/src/next.ts +188 -0
- package/src/react/index.tsx +6 -6
- package/src/react/server.ts +37 -0
- package/src/runtime/copy.ts +22 -3
- package/src/runtime/helpers.ts +54 -18
- package/src/runtime/locale.ts +19 -0
- package/src/runtime/types.ts +21 -6
- package/src/tools/bundle.ts +131 -0
- package/src/tools/catalog.ts +2 -2
- package/src/tools/compile.ts +60 -23
- package/src/tools/config.ts +39 -0
- package/src/tools/endpoints.ts +121 -0
- package/src/tools/entry.ts +77 -0
- package/src/tools/generate.ts +2 -0
- package/src/tools/icu.ts +63 -7
- package/src/tools/index.ts +6 -3
- package/src/tools/lock.ts +7 -0
- package/src/tools/mf1.ts +21 -7
- package/src/tools/mf2.ts +249 -57
- package/src/tools/migrate.ts +3 -2
- package/src/tools/native/emit.ts +87 -0
- package/src/tools/native/index.ts +36 -0
- package/src/tools/native/kotlin.ts +269 -0
- package/src/tools/native/model.ts +280 -0
- package/src/tools/native/number.ts +210 -0
- package/src/tools/native/plurals.ts +103 -0
- package/src/tools/native/rust.ts +367 -0
- package/src/tools/native/swift.ts +326 -0
- package/src/tools/syntax.ts +3 -3
- package/src/tools/todo.ts +118 -0
- package/src/vite.ts +28 -114
- package/dist/config-D--RITk2.js.map +0 -1
- package/dist/config-_4HfnJQu.d.ts.map +0 -1
- package/dist/copy-DXnzXsJa.d.ts.map +0 -1
- package/dist/helpers-BPuPGjQP.js +0 -61
- package/dist/helpers-BPuPGjQP.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/sarif-CI8HOokm.js.map +0 -1
- package/dist/scan-Ch2BfIUD.js +0 -2561
- package/dist/scan-Ch2BfIUD.js.map +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A number format taken apart at build time, so native code can put it back together with no
|
|
3
|
+
* formatter of its own: the digits to round to, the symbols, how integers group, and the text around
|
|
4
|
+
* the number for each sign. Read from `Intl.NumberFormat` and checked against it before any code is
|
|
5
|
+
* written, so a native target says what the browser says, or the build says why it can't.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The text before and after the number. */
|
|
9
|
+
export type Affix = readonly [prefix: string, suffix: string];
|
|
10
|
+
|
|
11
|
+
export interface NumberSpec {
|
|
12
|
+
minInt: number;
|
|
13
|
+
minFrac: number;
|
|
14
|
+
maxFrac: number;
|
|
15
|
+
/** How far the decimal point moves right before rounding: 2 for a percent. */
|
|
16
|
+
shift: number;
|
|
17
|
+
/** Integer digits grouped from the right, `primary` then `secondary` at a time, once there are `primary + min` of them; `min` 0 never groups. */
|
|
18
|
+
grouping: { primary: number; secondary: number; min: number };
|
|
19
|
+
decimal: string;
|
|
20
|
+
group: string;
|
|
21
|
+
/** The numbering system's digits, 0 to 9, where it isn't ASCII's. */
|
|
22
|
+
digits?: string[];
|
|
23
|
+
positive: Affix;
|
|
24
|
+
negative: Affix;
|
|
25
|
+
/** A value that rounds to zero, by its sign. */
|
|
26
|
+
zero: Affix;
|
|
27
|
+
negativeZero: Affix;
|
|
28
|
+
nan: string;
|
|
29
|
+
infinity: string;
|
|
30
|
+
negativeInfinity: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The plural operands' digits: rounded as `Intl.PluralRules` rounds before it selects. */
|
|
34
|
+
export interface Rounding {
|
|
35
|
+
minFrac: number;
|
|
36
|
+
maxFrac: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NUMBER_PARTS = new Set(['integer', 'group', 'decimal', 'fraction']);
|
|
40
|
+
|
|
41
|
+
/** Values the spec must format as `Intl` does: signs, zeros, ties, grouping thresholds, every plural form a unit could take. */
|
|
42
|
+
const SAMPLES = [
|
|
43
|
+
0,
|
|
44
|
+
-0,
|
|
45
|
+
1,
|
|
46
|
+
-1,
|
|
47
|
+
2,
|
|
48
|
+
3,
|
|
49
|
+
5,
|
|
50
|
+
11,
|
|
51
|
+
21,
|
|
52
|
+
0.5,
|
|
53
|
+
1.5,
|
|
54
|
+
2.5,
|
|
55
|
+
-2.5,
|
|
56
|
+
0.125,
|
|
57
|
+
0.0001,
|
|
58
|
+
-0.0001,
|
|
59
|
+
0.07,
|
|
60
|
+
1.005,
|
|
61
|
+
12.345,
|
|
62
|
+
999.9999,
|
|
63
|
+
1000,
|
|
64
|
+
10000,
|
|
65
|
+
100000,
|
|
66
|
+
1234.5678,
|
|
67
|
+
-1234.5678,
|
|
68
|
+
1e6,
|
|
69
|
+
123456789.987,
|
|
70
|
+
1e21,
|
|
71
|
+
NaN,
|
|
72
|
+
Infinity,
|
|
73
|
+
-Infinity,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function affixOf(parts: Intl.NumberFormatPart[]): Affix {
|
|
77
|
+
const first = parts.findIndex((p) => NUMBER_PARTS.has(p.type));
|
|
78
|
+
const last = parts.findLastIndex((p) => NUMBER_PARTS.has(p.type));
|
|
79
|
+
const text = (from: number, to: number) =>
|
|
80
|
+
parts
|
|
81
|
+
.slice(from, to)
|
|
82
|
+
.map((p) => p.value)
|
|
83
|
+
.join('');
|
|
84
|
+
return [text(0, first), text(last + 1, parts.length)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function partOf(parts: Intl.NumberFormatPart[], type: string): string | undefined {
|
|
88
|
+
return parts.find((p) => p.type === type)?.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What a format with `options` takes in `locale`. Throws a sentence saying what a native target
|
|
93
|
+
* can't reproduce, which the caller puts in front of a fix.
|
|
94
|
+
*/
|
|
95
|
+
export function numberSpec(locale: string, options: Intl.NumberFormatOptions = {}): NumberSpec {
|
|
96
|
+
const format = new Intl.NumberFormat(locale, options);
|
|
97
|
+
const resolved = format.resolvedOptions();
|
|
98
|
+
if (resolved.notation !== 'standard') throw new Error(`${resolved.notation} notation reads the size of the number at runtime`);
|
|
99
|
+
if (resolved.minimumSignificantDigits !== undefined) throw new Error('significant digits round by the size of the number');
|
|
100
|
+
if (resolved.roundingIncrement !== 1 || resolved.roundingMode !== 'halfExpand')
|
|
101
|
+
throw new Error('it rounds other than half away from zero');
|
|
102
|
+
if (resolved.trailingZeroDisplay !== 'auto') throw new Error('it drops trailing zeros by value');
|
|
103
|
+
|
|
104
|
+
const at = (more: Intl.NumberFormatOptions) => new Intl.NumberFormat(locale, { ...options, ...more });
|
|
105
|
+
const whole = at({ minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
106
|
+
const integers = whole
|
|
107
|
+
.formatToParts(1234567891234)
|
|
108
|
+
.filter((p) => p.type === 'integer')
|
|
109
|
+
.map((p) => p.value.length);
|
|
110
|
+
const grouped = (digits: number) =>
|
|
111
|
+
whole.formatToParts(10 ** (digits - 1 - (resolved.style === 'percent' ? 2 : 0))).some((p) => p.type === 'group');
|
|
112
|
+
const primary = integers.at(-1) ?? 3;
|
|
113
|
+
const secondary = integers.length > 2 ? integers.at(-2)! : primary;
|
|
114
|
+
const min = integers.length < 2 ? 0 : ([1, 2, 3, 4].find((m) => grouped(primary + m)) ?? 0);
|
|
115
|
+
|
|
116
|
+
const native = new Intl.NumberFormat(locale, { numberingSystem: resolved.numberingSystem, useGrouping: false });
|
|
117
|
+
const digits = Array.from({ length: 10 }, (_, d) => native.format(d));
|
|
118
|
+
const spec: NumberSpec = {
|
|
119
|
+
minInt: resolved.minimumIntegerDigits,
|
|
120
|
+
minFrac: resolved.minimumFractionDigits ?? 0,
|
|
121
|
+
maxFrac: resolved.maximumFractionDigits ?? 0,
|
|
122
|
+
shift: resolved.style === 'percent' ? 2 : 0,
|
|
123
|
+
grouping: { primary, secondary, min },
|
|
124
|
+
decimal: partOf(at({ minimumFractionDigits: 1, maximumFractionDigits: 1 }).formatToParts(1.5), 'decimal') ?? '.',
|
|
125
|
+
group: partOf(whole.formatToParts(1234567891234), 'group') ?? ',',
|
|
126
|
+
...(digits.join('') !== '0123456789' && { digits }),
|
|
127
|
+
positive: affixOf(format.formatToParts(1)),
|
|
128
|
+
negative: affixOf(format.formatToParts(-1)),
|
|
129
|
+
zero: affixOf(format.formatToParts(0)),
|
|
130
|
+
negativeZero: affixOf(format.formatToParts(-0)),
|
|
131
|
+
nan: format.format(NaN),
|
|
132
|
+
infinity: format.format(Infinity),
|
|
133
|
+
negativeInfinity: format.format(-Infinity),
|
|
134
|
+
};
|
|
135
|
+
for (const sample of SAMPLES) {
|
|
136
|
+
const expected = format.format(sample);
|
|
137
|
+
const actual = formatNumber(spec, sample);
|
|
138
|
+
if (actual !== expected) throw new Error(`it writes ${sample} as "${expected}", which native code would write as "${actual}"`);
|
|
139
|
+
}
|
|
140
|
+
return spec;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** How `Intl.PluralRules` rounds before it selects, with `options` (MF2's selector options) in `locale`. */
|
|
144
|
+
export function roundingOf(locale: string, options: Intl.PluralRulesOptions = {}): Rounding {
|
|
145
|
+
const resolved = new Intl.PluralRules(locale, options).resolvedOptions();
|
|
146
|
+
if (resolved.minimumSignificantDigits !== undefined) throw new Error('significant digits select by the size of the number');
|
|
147
|
+
return { minFrac: resolved.minimumFractionDigits ?? 0, maxFrac: resolved.maximumFractionDigits ?? 3 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The digits of a finite, non-negative double as the shortest decimal that reads back as it, which
|
|
152
|
+
* is what `Intl` rounds: 1.005 is 1.00499… in binary, and still rounds to 1.01. Every target's
|
|
153
|
+
* shortest form (`String()` here) may use an exponent, which moves the point.
|
|
154
|
+
*/
|
|
155
|
+
export function shortestDigits(value: number): { int: string; frac: string } {
|
|
156
|
+
const [mantissa = '', exponent = '0'] = String(value).split('e');
|
|
157
|
+
const [int = '', frac = ''] = mantissa.split('.');
|
|
158
|
+
const point = int.length + Number(exponent);
|
|
159
|
+
const all = int + frac;
|
|
160
|
+
if (point <= 0) return { int: '0', frac: '0'.repeat(-point) + all };
|
|
161
|
+
return { int: all.slice(0, point).padEnd(point, '0'), frac: all.slice(point) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Exact digits moved `shift` places right, rounded half away from zero to `maxFrac`, with trailing
|
|
166
|
+
* zeros cut down to `minFrac`. The algorithm every target writes out; keep them in step.
|
|
167
|
+
*/
|
|
168
|
+
export function roundDigits(int: string, frac: string, shift: number, minFrac: number, maxFrac: number): { int: string; frac: string } {
|
|
169
|
+
const padded = frac.padEnd(shift, '0');
|
|
170
|
+
const whole = int + padded.slice(0, shift);
|
|
171
|
+
const rest = padded.slice(shift);
|
|
172
|
+
const kept = (whole + rest.slice(0, maxFrac).padEnd(maxFrac, '0')).split('').map(Number);
|
|
173
|
+
if ((rest[maxFrac] ?? '0') >= '5') {
|
|
174
|
+
let at = kept.length - 1;
|
|
175
|
+
while (at >= 0 && kept[at] === 9) kept[at--] = 0;
|
|
176
|
+
if (at < 0) kept.unshift(1);
|
|
177
|
+
else kept[at]!++;
|
|
178
|
+
}
|
|
179
|
+
const all = kept.join('');
|
|
180
|
+
let outFrac = all.slice(all.length - maxFrac);
|
|
181
|
+
while (outFrac.length > minFrac && outFrac.endsWith('0')) outFrac = outFrac.slice(0, -1);
|
|
182
|
+
return { int: all.slice(0, all.length - maxFrac).replace(/^0+/, ''), frac: outFrac };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function groupDigits(int: string, { primary, secondary, min }: NumberSpec['grouping'], separator: string): string {
|
|
186
|
+
if (min === 0 || int.length < primary + min) return int;
|
|
187
|
+
const groups = [int.slice(-primary)];
|
|
188
|
+
let rest = int.slice(0, -primary);
|
|
189
|
+
while (rest.length > secondary) {
|
|
190
|
+
groups.unshift(rest.slice(-secondary));
|
|
191
|
+
rest = rest.slice(0, -secondary);
|
|
192
|
+
}
|
|
193
|
+
groups.unshift(rest);
|
|
194
|
+
return groups.join(separator);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `value` as the spec writes it: the reference for the code each target generates. */
|
|
198
|
+
export function formatNumber(spec: NumberSpec, value: number): string {
|
|
199
|
+
if (Number.isNaN(value)) return spec.nan;
|
|
200
|
+
if (!Number.isFinite(value)) return value > 0 ? spec.infinity : spec.negativeInfinity;
|
|
201
|
+
const shortest = shortestDigits(Math.abs(value));
|
|
202
|
+
const { int, frac } = roundDigits(shortest.int, shortest.frac, spec.shift, spec.minFrac, spec.maxFrac);
|
|
203
|
+
const negative = value < 0 || Object.is(value, -0);
|
|
204
|
+
const zero = /^0*$/.test(int + frac);
|
|
205
|
+
const [prefix, suffix] = zero ? (negative ? spec.negativeZero : spec.zero) : negative ? spec.negative : spec.positive;
|
|
206
|
+
const shown =
|
|
207
|
+
groupDigits(int.padStart(Math.max(spec.minInt, 1), '0'), spec.grouping, spec.group) + (frac === '' ? '' : spec.decimal + frac);
|
|
208
|
+
const digits = spec.digits;
|
|
209
|
+
return prefix + (digits === undefined ? shown : shown.replace(/[0-9]/g, (d) => digits[Number(d)]!)) + suffix;
|
|
210
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLDR's plural rules, read into conditions a native target writes as code. Swift, the JVM and Rust
|
|
3
|
+
* have no plural rules of their own that every platform carries, so each generated file brings the
|
|
4
|
+
* rules of the locales it formats, from the same CLDR data `Intl.PluralRules` is built on.
|
|
5
|
+
*/
|
|
6
|
+
import ordinals from 'cldr-core/supplemental/ordinals.json' with { type: 'json' };
|
|
7
|
+
import cardinals from 'cldr-core/supplemental/plurals.json' with { type: 'json' };
|
|
8
|
+
|
|
9
|
+
/** CLDR's operands: `n` the absolute value, `i` its integer digits, `v`/`w` the count of fraction digits with and without trailing zeros, `f`/`t` those digits, `c`/`e` the compact exponent. */
|
|
10
|
+
export type Operand = 'n' | 'i' | 'v' | 'w' | 'f' | 't' | 'c' | 'e';
|
|
11
|
+
|
|
12
|
+
/** `i % 10 = 2..4, 7`: the operand, less its modulus, in (or with `negated`, not in) the ranges. A single value is a range of one. */
|
|
13
|
+
export interface Relation {
|
|
14
|
+
operand: Operand;
|
|
15
|
+
mod?: number;
|
|
16
|
+
negated: boolean;
|
|
17
|
+
ranges: (readonly [number, number])[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Relations joined by `and`, those joined by `or`. */
|
|
21
|
+
export type Condition = Relation[][];
|
|
22
|
+
|
|
23
|
+
/** A locale's categories in CLDR's order, each with its condition. `other` holds wherever none does, so it has none. */
|
|
24
|
+
export type PluralRules = (readonly [category: string, Condition])[];
|
|
25
|
+
|
|
26
|
+
export type Operands = Record<Operand, number>;
|
|
27
|
+
|
|
28
|
+
type RuleData = Record<string, Record<string, string>>;
|
|
29
|
+
|
|
30
|
+
const DATA = {
|
|
31
|
+
cardinal: cardinals.supplemental['plurals-type-cardinal'] as RuleData,
|
|
32
|
+
ordinal: ordinals.supplemental['plurals-type-ordinal'] as RuleData,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The rules for `locale`, found as `Intl.PluralRules` finds its locale, then by its parents
|
|
37
|
+
* (`pt-PT`, then `pt`), so the generated code picks the category the browser would.
|
|
38
|
+
*/
|
|
39
|
+
export function pluralRules(locale: string, type: 'cardinal' | 'ordinal'): PluralRules {
|
|
40
|
+
const data = DATA[type];
|
|
41
|
+
let tag = new Intl.PluralRules(locale, { type }).resolvedOptions().locale;
|
|
42
|
+
while (!(tag in data) && tag.includes('-')) tag = tag.slice(0, tag.lastIndexOf('-'));
|
|
43
|
+
const rules = data[tag] ?? data['root'] ?? {};
|
|
44
|
+
return Object.entries(rules).flatMap(([name, rule]) => {
|
|
45
|
+
const category = name.slice('pluralRule-count-'.length);
|
|
46
|
+
return category === 'other' ? [] : [[category, parseCondition(rule.split('@', 1)[0]!)] as const];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `v = 0 and i % 10 = 1 or n = 0..1`, its samples already cut. */
|
|
51
|
+
export function parseCondition(source: string): Condition {
|
|
52
|
+
return source
|
|
53
|
+
.trim()
|
|
54
|
+
.split(/\s+or\s+/)
|
|
55
|
+
.map((and) => and.split(/\s+and\s+/).map(parseRelation));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseRelation(source: string): Relation {
|
|
59
|
+
const match = /^([nivwftce])\s*(?:%\s*(\d+))?\s*(!?=)\s*(.+)$/.exec(source.trim());
|
|
60
|
+
if (match === null) throw new Error(`prosebook: a CLDR plural rule it can't read: "${source}"`);
|
|
61
|
+
const [, operand, mod, op, list] = match;
|
|
62
|
+
return {
|
|
63
|
+
operand: operand as Operand,
|
|
64
|
+
...(mod !== undefined && { mod: Number(mod) }),
|
|
65
|
+
negated: op === '!=',
|
|
66
|
+
ranges: list!.split(',').map((range) => {
|
|
67
|
+
const [from, to = from] = range.trim().split('..');
|
|
68
|
+
return [Number(from), Number(to)] as const;
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A range of one is an equality; a wider range holds integers only, as CLDR's `in` does. */
|
|
74
|
+
export function inRange(value: number, [from, to]: readonly [number, number]): boolean {
|
|
75
|
+
return from === to ? value === from : Number.isInteger(value) && value >= from && value <= to;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function holds(relation: Relation, operands: Operands): boolean {
|
|
79
|
+
const raw = operands[relation.operand];
|
|
80
|
+
const value = relation.mod === undefined ? raw : raw % relation.mod;
|
|
81
|
+
return relation.ranges.some((range) => inRange(value, range)) !== relation.negated;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The category `rules` give, as the generated code picks it. */
|
|
85
|
+
export function categoryOf(rules: PluralRules, operands: Operands): string {
|
|
86
|
+
for (const [category, condition] of rules) if (condition.some((and) => and.every((r) => holds(r, operands)))) return category;
|
|
87
|
+
return 'other';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The operands of a number written with `int` and `frac` digits, as CLDR reads them. */
|
|
91
|
+
export function operandsOf(int: string, frac: string): Operands {
|
|
92
|
+
const trimmed = frac.replace(/0+$/, '');
|
|
93
|
+
return {
|
|
94
|
+
n: Number(`${int}.${frac || '0'}`),
|
|
95
|
+
i: Number(int),
|
|
96
|
+
v: frac.length,
|
|
97
|
+
w: trimmed.length,
|
|
98
|
+
f: Number(frac || '0'),
|
|
99
|
+
t: Number(trimmed || '0'),
|
|
100
|
+
c: 0,
|
|
101
|
+
e: 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model as a Rust module: an enum of locales with a method per message, and what formats them in
|
|
3
|
+
* a private `rt` module, so no argument a message names can shadow it.
|
|
4
|
+
*/
|
|
5
|
+
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { checkNames, conditionCode, float, leading, pascal, snake, stringLiteral, wordsOf } from './emit.ts';
|
|
7
|
+
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
|
+
import type { NumberSpec } from './number.ts';
|
|
9
|
+
|
|
10
|
+
const KEYWORDS = new Set(
|
|
11
|
+
'as async await break const continue crate dyn 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 abstract become box do final macro override priv try typeof unsized virtual yield gen'.split(
|
|
12
|
+
' ',
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const ident = (name: string) => (KEYWORDS.has(name) ? `${name}_` : name);
|
|
17
|
+
const lit = (value: string) => stringLiteral(value, 'rust');
|
|
18
|
+
const variant = (tag: string) => pascal(wordsOf(tag));
|
|
19
|
+
const argName = (name: string) => ident(leading(snake(wordsOf(name))));
|
|
20
|
+
|
|
21
|
+
function doc(lines: string[], indent: string): string {
|
|
22
|
+
return lines.map((l) => (l === '' ? `${indent}///\n` : `${indent}/// ${l}\n`)).join('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The parts as an expression of type `String`. */
|
|
26
|
+
function expr(parts: Part[], indent: string): string {
|
|
27
|
+
if (parts.length === 0) return 'String::new()';
|
|
28
|
+
const [only] = parts;
|
|
29
|
+
if (parts.length === 1 && only !== undefined) {
|
|
30
|
+
if (only.type === 'text') return `String::from(${lit(only.value)})`;
|
|
31
|
+
return only.type === 'string' ? `${argName(only.arg)}.to_string()` : value(only, indent);
|
|
32
|
+
}
|
|
33
|
+
let format = '';
|
|
34
|
+
const args: string[] = [];
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
if (part.type === 'text') {
|
|
37
|
+
format += part.value.replaceAll('{', '{{').replaceAll('}', '}}');
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
format += '{}';
|
|
41
|
+
args.push(value(part, indent));
|
|
42
|
+
}
|
|
43
|
+
return `format!(${[lit(format), ...args].join(', ')})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function value(part: Exclude<Part, { type: 'text' }>, indent: string): string {
|
|
47
|
+
const inner = `${indent} `;
|
|
48
|
+
if (part.type === 'string') return argName(part.arg);
|
|
49
|
+
if (part.type === 'number') {
|
|
50
|
+
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
51
|
+
return `rt::number(${of}, &rt::F${part.format}, ${lit(part.fallback)})`;
|
|
52
|
+
}
|
|
53
|
+
if (part.type === 'select') {
|
|
54
|
+
const arms = part.arms.map((a) => `${inner}${lit(a.key)} => ${expr(a.parts, inner)},\n`).join('');
|
|
55
|
+
return `match ${argName(part.arg)} {\n${arms}${inner}_ => ${expr(part.other, inner)},\n${indent}}`;
|
|
56
|
+
}
|
|
57
|
+
// A plural, the one kind left. With only `other`, there is nothing to select on.
|
|
58
|
+
if (part.arms.length + part.exact.length === 0) return expr(part.other, indent);
|
|
59
|
+
const arg = argName(part.arg);
|
|
60
|
+
const on = part.on === 'round' ? `rt::round(${arg})` : part.on === 'percent' ? `${arg} * 100.0` : arg;
|
|
61
|
+
const { minFrac, maxFrac } = part.rounding;
|
|
62
|
+
const exact = part.exact.map((e) => `(${float(e.value)}, ${lit(`=${e.value}`)})`).join(', ');
|
|
63
|
+
const arms = [...part.exact.map((e) => ({ key: `=${e.value}`, parts: e.parts })), ...part.arms]
|
|
64
|
+
.map((a) => `${inner}${lit(a.key)} => ${expr(a.parts, inner)},\n`)
|
|
65
|
+
.join('');
|
|
66
|
+
const pick = `rt::plural(${on}, ${float(part.offset)}, &[${exact}], rt::R${part.rules}, ${minFrac}, ${maxFrac})`;
|
|
67
|
+
return `match ${pick} {\n${arms}${inner}_ => ${expr(part.other, inner)},\n${indent}}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function method(m: NativeMessage, name: string, enumName: string): string {
|
|
71
|
+
const head = doc(m.doc, ' ');
|
|
72
|
+
const arms = (render: (parts: Part[]) => string) =>
|
|
73
|
+
m.bodies.map((b) => ` ${b.locales.map((l) => `${enumName}::${variant(l)}`).join(' | ')} => ${render(b.parts)},\n`).join('');
|
|
74
|
+
const isStatic = m.bodies.every((b) => b.parts.every((p) => p.type === 'text'));
|
|
75
|
+
if (isStatic && m.args.length === 0)
|
|
76
|
+
return `${head} pub fn ${name}(self) -> &'static str {\n match self {\n${arms((parts) => lit(parts[0]?.type === 'text' ? parts[0].value : ''))} }\n }\n`;
|
|
77
|
+
const params = m.args.map((a) => `, ${argName(a.name)}: ${a.type === 'number' ? 'impl Number' : '&str'}`).join('');
|
|
78
|
+
const argDocs = m.args.filter((a) => a.doc !== undefined).map((a) => `* \`${argName(a.name)}\`: ${a.doc}`);
|
|
79
|
+
const docs = argDocs.length > 0 ? `${head} ///\n${doc(argDocs, ' ')}` : head;
|
|
80
|
+
const numbers = m.args
|
|
81
|
+
.filter((a) => a.type === 'number')
|
|
82
|
+
.map((a) => ` let ${argName(a.name)} = ${argName(a.name)}.as_f64();\n`)
|
|
83
|
+
.join('');
|
|
84
|
+
return `${docs} pub fn ${name}(self${params}) -> String {\n${numbers} match self {\n${arms((parts) => expr(parts, ' '))} }\n }\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatConst(spec: NumberSpec, i: number): string {
|
|
88
|
+
const affix = ([prefix, suffix]: readonly [string, string]) => `(${lit(prefix)}, ${lit(suffix)})`;
|
|
89
|
+
const fields = [
|
|
90
|
+
`min_int: ${spec.minInt}`,
|
|
91
|
+
`min_frac: ${spec.minFrac}`,
|
|
92
|
+
`max_frac: ${spec.maxFrac}`,
|
|
93
|
+
`shift: ${spec.shift}`,
|
|
94
|
+
`primary: ${spec.grouping.primary}`,
|
|
95
|
+
`secondary: ${spec.grouping.secondary}`,
|
|
96
|
+
`min_group: ${spec.grouping.min}`,
|
|
97
|
+
`decimal: ${lit(spec.decimal)}`,
|
|
98
|
+
`group: ${lit(spec.group)}`,
|
|
99
|
+
`digits: ${spec.digits === undefined ? 'None' : `Some([${spec.digits.map(lit).join(', ')}])`}`,
|
|
100
|
+
`positive: ${affix(spec.positive)}`,
|
|
101
|
+
`negative: ${affix(spec.negative)}`,
|
|
102
|
+
`zero: ${affix(spec.zero)}`,
|
|
103
|
+
`negative_zero: ${affix(spec.negativeZero)}`,
|
|
104
|
+
`infinity: ${lit(spec.infinity)}`,
|
|
105
|
+
`negative_infinity: ${lit(spec.negativeInfinity)}`,
|
|
106
|
+
];
|
|
107
|
+
return ` pub const F${i}: Format = Format {\n${fields.map((f) => ` ${f},\n`).join('')} };\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const NUMBER = `
|
|
111
|
+
/// A number format, taken apart when the file was generated.
|
|
112
|
+
pub struct Format {
|
|
113
|
+
pub min_int: usize,
|
|
114
|
+
pub min_frac: usize,
|
|
115
|
+
pub max_frac: usize,
|
|
116
|
+
pub shift: usize,
|
|
117
|
+
pub primary: usize,
|
|
118
|
+
pub secondary: usize,
|
|
119
|
+
pub min_group: usize,
|
|
120
|
+
pub decimal: &'static str,
|
|
121
|
+
pub group: &'static str,
|
|
122
|
+
pub digits: Option<[&'static str; 10]>,
|
|
123
|
+
pub positive: (&'static str, &'static str),
|
|
124
|
+
pub negative: (&'static str, &'static str),
|
|
125
|
+
pub zero: (&'static str, &'static str),
|
|
126
|
+
pub negative_zero: (&'static str, &'static str),
|
|
127
|
+
pub infinity: &'static str,
|
|
128
|
+
pub negative_infinity: &'static str,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// \`value\` as \`format\` writes it, or \`fallback\` where it isn't a number.
|
|
132
|
+
pub fn number(value: f64, format: &Format, fallback: &str) -> String {
|
|
133
|
+
if value.is_nan() {
|
|
134
|
+
return fallback.to_string();
|
|
135
|
+
}
|
|
136
|
+
if value.is_infinite() {
|
|
137
|
+
return (if value > 0.0 { format.infinity } else { format.negative_infinity }).to_string();
|
|
138
|
+
}
|
|
139
|
+
let (mut int, frac) = digits(value.abs(), format.shift, format.min_frac, format.max_frac);
|
|
140
|
+
let zero = int.bytes().chain(frac.bytes()).all(|b| b == b'0');
|
|
141
|
+
let (prefix, suffix) = match (zero, value.is_sign_negative()) {
|
|
142
|
+
(true, true) => format.negative_zero,
|
|
143
|
+
(true, false) => format.zero,
|
|
144
|
+
(false, true) => format.negative,
|
|
145
|
+
(false, false) => format.positive,
|
|
146
|
+
};
|
|
147
|
+
while int.len() < format.min_int.max(1) {
|
|
148
|
+
int.insert(0, '0');
|
|
149
|
+
}
|
|
150
|
+
let grouped = format.min_group > 0 && int.len() >= format.primary + format.min_group;
|
|
151
|
+
let mut out = String::from(prefix);
|
|
152
|
+
for (i, digit) in int.bytes().enumerate() {
|
|
153
|
+
let left = int.len() - i;
|
|
154
|
+
if grouped && i > 0 && (left == format.primary || (left > format.primary && (left - format.primary) % format.secondary == 0)) {
|
|
155
|
+
out.push_str(format.group);
|
|
156
|
+
}
|
|
157
|
+
push_digit(&mut out, digit, format);
|
|
158
|
+
}
|
|
159
|
+
if !frac.is_empty() {
|
|
160
|
+
out.push_str(format.decimal);
|
|
161
|
+
for digit in frac.bytes() {
|
|
162
|
+
push_digit(&mut out, digit, format);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
out.push_str(suffix);
|
|
166
|
+
out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn push_digit(out: &mut String, digit: u8, format: &Format) {
|
|
170
|
+
match format.digits {
|
|
171
|
+
Some(digits) => out.push_str(digits[(digit - b'0') as usize]),
|
|
172
|
+
None => out.push(digit as char),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// The shortest digits that read back as \`value\`, as \`Intl\` rounds them: moved \`shift\` places
|
|
177
|
+
/// right, rounded half away from zero to \`max\` fraction digits, and trailing zeros cut to \`min\`.
|
|
178
|
+
pub fn digits(value: f64, shift: usize, min: usize, max: usize) -> (String, String) {
|
|
179
|
+
// Display writes the shortest digits that read back, and never an exponent.
|
|
180
|
+
let shortest = value.to_string();
|
|
181
|
+
let (int, frac) = shortest.split_once('.').unwrap_or((&shortest, ""));
|
|
182
|
+
let mut all: Vec<u8> = int.bytes().map(|b| b - b'0').collect();
|
|
183
|
+
let mut rest = frac.bytes().map(|b| b - b'0');
|
|
184
|
+
let mut point = all.len() + shift;
|
|
185
|
+
for _ in 0..shift + max {
|
|
186
|
+
all.push(rest.next().unwrap_or(0));
|
|
187
|
+
}
|
|
188
|
+
if rest.next().unwrap_or(0) >= 5 {
|
|
189
|
+
let mut at = all.len();
|
|
190
|
+
loop {
|
|
191
|
+
if at == 0 {
|
|
192
|
+
all.insert(0, 1);
|
|
193
|
+
point += 1;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
at -= 1;
|
|
197
|
+
if all[at] == 9 {
|
|
198
|
+
all[at] = 0;
|
|
199
|
+
} else {
|
|
200
|
+
all[at] += 1;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
let mut frac = all.split_off(point);
|
|
206
|
+
while frac.len() > min && frac.last() == Some(&0) {
|
|
207
|
+
frac.pop();
|
|
208
|
+
}
|
|
209
|
+
let first = all.iter().position(|&d| d != 0).unwrap_or(all.len());
|
|
210
|
+
let text = |d: &[u8]| d.iter().map(|&d| (b'0' + d) as char).collect::<String>();
|
|
211
|
+
(text(&all[first..]), text(&frac))
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const PLURAL = `
|
|
216
|
+
/// CLDR's plural operands of a number as written.
|
|
217
|
+
pub struct Operands {
|
|
218
|
+
pub n: f64,
|
|
219
|
+
pub i: f64,
|
|
220
|
+
pub v: f64,
|
|
221
|
+
pub w: f64,
|
|
222
|
+
pub f: f64,
|
|
223
|
+
pub t: f64,
|
|
224
|
+
pub c: f64,
|
|
225
|
+
pub e: f64,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// The arm a plural takes: the \`=N\` that \`value\` equals, else the category of \`value\` less \`offset\`.
|
|
229
|
+
pub fn plural(
|
|
230
|
+
value: f64,
|
|
231
|
+
offset: f64,
|
|
232
|
+
exact: &[(f64, &'static str)],
|
|
233
|
+
rules: fn(&Operands) -> &'static str,
|
|
234
|
+
min: usize,
|
|
235
|
+
max: usize,
|
|
236
|
+
) -> &'static str {
|
|
237
|
+
match exact.iter().find(|(n, _)| *n == value) {
|
|
238
|
+
Some(&(_, arm)) => arm,
|
|
239
|
+
None => category(rules, value - offset, min, max),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// The category \`rules\` give \`value\`, rounded to between \`min\` and \`max\` fraction digits.
|
|
244
|
+
fn category(rules: fn(&Operands) -> &'static str, value: f64, min: usize, max: usize) -> &'static str {
|
|
245
|
+
if !value.is_finite() {
|
|
246
|
+
return "other";
|
|
247
|
+
}
|
|
248
|
+
let (int, frac) = digits(value.abs(), 0, min, max);
|
|
249
|
+
let int = if int.is_empty() { String::from("0") } else { int };
|
|
250
|
+
let trimmed = frac.trim_end_matches('0');
|
|
251
|
+
let read = |s: &str| if s.is_empty() { 0.0 } else { s.parse().unwrap_or(0.0) };
|
|
252
|
+
rules(&Operands {
|
|
253
|
+
n: if frac.is_empty() { read(&int) } else { read(&format!("{}.{}", int, frac)) },
|
|
254
|
+
i: read(&int),
|
|
255
|
+
v: frac.len() as f64,
|
|
256
|
+
w: trimmed.len() as f64,
|
|
257
|
+
f: read(&frac),
|
|
258
|
+
t: read(trimmed),
|
|
259
|
+
c: 0.0,
|
|
260
|
+
e: 0.0,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// An integer in the range, as CLDR's ranges hold.
|
|
265
|
+
fn within(value: f64, from: f64, to: f64) -> bool {
|
|
266
|
+
value.fract() == 0.0 && value >= from && value <= to
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// \`Math.round\`: half up, toward positive infinity.
|
|
270
|
+
pub fn round(value: f64) -> f64 {
|
|
271
|
+
let floor = value.floor();
|
|
272
|
+
if value - floor >= 0.5 { floor + 1.0 } else { floor }
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
const NUMBER_TRAIT = `
|
|
277
|
+
/// A number a message formats: any of Rust's integers or floats.
|
|
278
|
+
pub trait Number: Copy {
|
|
279
|
+
fn as_f64(self) -> f64;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
macro_rules! number {
|
|
283
|
+
($($t:ty),*) => {$(
|
|
284
|
+
impl Number for $t {
|
|
285
|
+
fn as_f64(self) -> f64 {
|
|
286
|
+
self as f64
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
)*};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
number!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
export function rustModule(model: NativeModel, target: NativeTarget & { name: string }, banner: string): string {
|
|
296
|
+
const name = target.name;
|
|
297
|
+
const names = model.messages.map((m): [string, string] => [m.key, ident(leading(snake(m.words)))]);
|
|
298
|
+
checkNames('rust', names, ['tag', 'negotiate']);
|
|
299
|
+
const variants = model.locales.map((l) => ` /// \`${l}\`${l === model.source ? ', the source' : ''}.\n ${variant(l)},\n`).join('');
|
|
300
|
+
const tags = model.locales.map((l) => ` ${name}::${variant(l)} => ${lit(l)},\n`).join('');
|
|
301
|
+
const methods = model.messages.map((m, i) => method(m, names[i]![1], name)).join('\n');
|
|
302
|
+
const usesNumbers = model.messages.some((m) => m.args.some((a) => a.type === 'number'));
|
|
303
|
+
const rules = model.rules
|
|
304
|
+
.map(({ name: rule, rules }, i) => {
|
|
305
|
+
const checks = rules.map(
|
|
306
|
+
([category, condition]) =>
|
|
307
|
+
` if ${conditionCode(condition, { field: (op) => `o.${op}`, mod: (v, by) => `${v} % ${by}`, within: (v, from, to) => `within(${v}, ${from}, ${to})` })} {\n return ${lit(category)};\n }\n`,
|
|
308
|
+
);
|
|
309
|
+
return ` /// ${rule}\n pub fn R${i}(${rules.length > 0 ? 'o' : '_o'}: &Operands) -> &'static str {\n${checks.join('')} "other"\n }\n`;
|
|
310
|
+
})
|
|
311
|
+
.join('\n');
|
|
312
|
+
const runtime =
|
|
313
|
+
model.formats.length + model.rules.length === 0
|
|
314
|
+
? ''
|
|
315
|
+
: `
|
|
316
|
+
/// What formats the messages, generated with them.
|
|
317
|
+
mod rt {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
318
|
+
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
return `${banner}
|
|
322
|
+
#![allow(dead_code, non_snake_case, clippy::all)]
|
|
323
|
+
${usesNumbers ? NUMBER_TRAIT : ''}
|
|
324
|
+
/// A locale the catalog is written or translated in, and its messages.
|
|
325
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
326
|
+
pub enum ${name} {
|
|
327
|
+
${variants}}
|
|
328
|
+
|
|
329
|
+
impl ${name} {
|
|
330
|
+
/// Every locale, the source first.
|
|
331
|
+
pub const ALL: [${name}; ${model.locales.length}] = [${model.locales.map((l) => `${name}::${variant(l)}`).join(', ')}];
|
|
332
|
+
|
|
333
|
+
/// The locale the catalog is written in, which every message falls back to.
|
|
334
|
+
pub const SOURCE: ${name} = ${name}::${variant(model.source)};
|
|
335
|
+
|
|
336
|
+
/// The locale's BCP 47 tag.
|
|
337
|
+
pub fn tag(self) -> &'static str {
|
|
338
|
+
match self {
|
|
339
|
+
${tags} }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// The best locale for what was asked, most preferred first: each tag tries an exact match, then
|
|
343
|
+
/// its parents (\`pt-BR\` finds \`pt\`), then any locale of its language. The source if none is.
|
|
344
|
+
pub fn negotiate<S: AsRef<str>>(requested: &[S]) -> ${name} {
|
|
345
|
+
let language = |tag: &str| tag.split('-').next().unwrap_or("").to_ascii_lowercase();
|
|
346
|
+
for want in requested {
|
|
347
|
+
let want = want.as_ref();
|
|
348
|
+
let mut tag = want;
|
|
349
|
+
loop {
|
|
350
|
+
if let Some(found) = Self::ALL.iter().find(|l| l.tag().eq_ignore_ascii_case(tag)) {
|
|
351
|
+
return *found;
|
|
352
|
+
}
|
|
353
|
+
match tag.rfind('-') {
|
|
354
|
+
Some(cut) => tag = &tag[..cut],
|
|
355
|
+
None => break,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if let Some(found) = Self::ALL.iter().find(|l| language(l.tag()) == language(want)) {
|
|
359
|
+
return *found;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
Self::SOURCE
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
${methods}}
|
|
366
|
+
${runtime}`;
|
|
367
|
+
}
|