prosebook 0.2.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.
Files changed (115) hide show
  1. package/README.md +345 -29
  2. package/dist/bun.d.ts +34 -0
  3. package/dist/bun.d.ts.map +1 -0
  4. package/dist/bun.js +98 -0
  5. package/dist/bun.js.map +1 -0
  6. package/dist/bundle-CpUIWb9j.d.ts +6 -0
  7. package/dist/bundle-CpUIWb9j.d.ts.map +1 -0
  8. package/dist/bundle-DboItAre.js +111 -0
  9. package/dist/bundle-DboItAre.js.map +1 -0
  10. package/dist/cli.js +51 -56
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compile-ClK9htKr.d.ts +16 -0
  13. package/dist/compile-ClK9htKr.d.ts.map +1 -0
  14. package/dist/{config-_4HfnJQu.d.ts → config-CzaJJQ60.d.ts} +75 -2
  15. package/dist/config-CzaJJQ60.d.ts.map +1 -0
  16. package/dist/{config-D--RITk2.js → config-nWZf9QmF.js} +42 -212
  17. package/dist/config-nWZf9QmF.js.map +1 -0
  18. package/dist/config.d.ts +2 -2
  19. package/dist/config.js +1 -1
  20. package/dist/{copy-DXnzXsJa.d.ts → copy-B3V_QRkT.d.ts} +20 -7
  21. package/dist/copy-B3V_QRkT.d.ts.map +1 -0
  22. package/dist/copy-Bo_FIoLy.js +154 -0
  23. package/dist/copy-Bo_FIoLy.js.map +1 -0
  24. package/dist/endpoints-BgG_5Vpm.js +96 -0
  25. package/dist/endpoints-BgG_5Vpm.js.map +1 -0
  26. package/dist/generate-BW3N8EGb.js +5488 -0
  27. package/dist/generate-BW3N8EGb.js.map +1 -0
  28. package/dist/helpers-BWGcD0kS.js +96 -0
  29. package/dist/helpers-BWGcD0kS.js.map +1 -0
  30. package/dist/icu-Dej0mNsF.js +218 -0
  31. package/dist/icu-Dej0mNsF.js.map +1 -0
  32. package/dist/index.d.ts +12 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -188
  35. package/dist/inspector.d.ts +4 -1
  36. package/dist/inspector.d.ts.map +1 -1
  37. package/dist/inspector.js +7 -2
  38. package/dist/inspector.js.map +1 -1
  39. package/dist/locale-DpQA1pqH.js +70 -0
  40. package/dist/locale-DpQA1pqH.js.map +1 -0
  41. package/dist/next-loader.d.ts +11 -0
  42. package/dist/next-loader.d.ts.map +1 -0
  43. package/dist/next-loader.js +17 -0
  44. package/dist/next-loader.js.map +1 -0
  45. package/dist/next.d.ts +30 -0
  46. package/dist/next.d.ts.map +1 -0
  47. package/dist/next.js +160 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/react/index.d.ts +3 -3
  50. package/dist/react/index.d.ts.map +1 -1
  51. package/dist/react/index.js.map +1 -1
  52. package/dist/react/server.d.ts +20 -0
  53. package/dist/react/server.d.ts.map +1 -0
  54. package/dist/react/server.js +34 -0
  55. package/dist/react/server.js.map +1 -0
  56. package/dist/{sarif-CI8HOokm.js → sarif-B5nrMqrT.js} +103 -87
  57. package/dist/sarif-B5nrMqrT.js.map +1 -0
  58. package/dist/scan-Bx0_yDSi.js +387 -0
  59. package/dist/scan-Bx0_yDSi.js.map +1 -0
  60. package/dist/tools.d.ts +83 -90
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +5 -4
  63. package/dist/vite.d.ts +3 -2
  64. package/dist/vite.d.ts.map +1 -1
  65. package/dist/vite.js +29 -109
  66. package/dist/vite.js.map +1 -1
  67. package/package.json +37 -4
  68. package/src/bun.ts +132 -0
  69. package/src/cli.ts +60 -58
  70. package/src/index.ts +1 -1
  71. package/src/inspector.ts +8 -3
  72. package/src/next-loader.ts +16 -0
  73. package/src/next.ts +188 -0
  74. package/src/react/index.tsx +6 -6
  75. package/src/react/server.ts +37 -0
  76. package/src/runtime/copy.ts +22 -3
  77. package/src/runtime/helpers.ts +54 -18
  78. package/src/runtime/locale.ts +19 -0
  79. package/src/runtime/types.ts +21 -6
  80. package/src/tools/bundle.ts +131 -0
  81. package/src/tools/catalog.ts +2 -2
  82. package/src/tools/compile.ts +60 -23
  83. package/src/tools/config.ts +57 -0
  84. package/src/tools/endpoints.ts +121 -0
  85. package/src/tools/entry.ts +77 -0
  86. package/src/tools/generate.ts +2 -0
  87. package/src/tools/icu.ts +63 -7
  88. package/src/tools/index.ts +6 -3
  89. package/src/tools/lock.ts +7 -0
  90. package/src/tools/mf1.ts +21 -7
  91. package/src/tools/mf2.ts +249 -57
  92. package/src/tools/migrate.ts +3 -2
  93. package/src/tools/native/android.ts +201 -0
  94. package/src/tools/native/apple.ts +139 -0
  95. package/src/tools/native/emit.ts +87 -0
  96. package/src/tools/native/index.ts +48 -0
  97. package/src/tools/native/kotlin.ts +269 -0
  98. package/src/tools/native/model.ts +292 -0
  99. package/src/tools/native/number.ts +210 -0
  100. package/src/tools/native/plurals.ts +103 -0
  101. package/src/tools/native/printf.ts +95 -0
  102. package/src/tools/native/rust.ts +367 -0
  103. package/src/tools/native/swift.ts +326 -0
  104. package/src/tools/syntax.ts +3 -3
  105. package/src/tools/todo.ts +118 -0
  106. package/src/vite.ts +28 -114
  107. package/dist/config-D--RITk2.js.map +0 -1
  108. package/dist/config-_4HfnJQu.d.ts.map +0 -1
  109. package/dist/copy-DXnzXsJa.d.ts.map +0 -1
  110. package/dist/helpers-BPuPGjQP.js +0 -61
  111. package/dist/helpers-BPuPGjQP.js.map +0 -1
  112. package/dist/index.js.map +0 -1
  113. package/dist/sarif-CI8HOokm.js.map +0 -1
  114. package/dist/scan-Ch2BfIUD.js +0 -2561
  115. package/dist/scan-Ch2BfIUD.js.map +0 -1
@@ -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
+ }