cricinfo-cli-go 0.1.2 → 0.1.4

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.
@@ -13,12 +13,15 @@ type matchCommandService interface {
13
13
  Close() error
14
14
  List(ctx context.Context, opts cricinfo.MatchListOptions) (cricinfo.NormalizedResult, error)
15
15
  Live(ctx context.Context, opts cricinfo.MatchListOptions) (cricinfo.NormalizedResult, error)
16
+ Lineups(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
16
17
  Show(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
17
18
  Status(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
18
19
  Scorecard(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
19
20
  Details(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
20
21
  Plays(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
21
22
  Situation(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
23
+ LiveView(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
24
+ Duel(ctx context.Context, query string, opts cricinfo.MatchDuelOptions) (cricinfo.NormalizedResult, error)
22
25
  Phases(ctx context.Context, query string, opts cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error)
23
26
  Innings(ctx context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error)
24
27
  Partnerships(ctx context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error)
@@ -30,6 +33,8 @@ type matchRuntimeOptions struct {
30
33
  limit int
31
34
  leagueID string
32
35
  team string
36
+ batter string
37
+ bowler string
33
38
  innings int
34
39
  period int
35
40
  }
@@ -52,6 +57,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
52
57
  " cricinfo matches list",
53
58
  " cricinfo matches show <match>",
54
59
  " cricinfo matches status <match>",
60
+ " cricinfo matches live-view <match>",
55
61
  " cricinfo matches scorecard <match>",
56
62
  " cricinfo matches innings <match>",
57
63
  }, "\n"),
@@ -73,6 +79,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
73
79
  " cricinfo matches show <match>",
74
80
  " cricinfo matches status <match>",
75
81
  " cricinfo matches scorecard <match>",
82
+ " cricinfo matches live-view <match>",
76
83
  " cricinfo matches innings <match>",
77
84
  }, "\n"),
78
85
  Args: cobra.NoArgs,
@@ -97,6 +104,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
97
104
  " cricinfo matches show <match>",
98
105
  " cricinfo matches status <match>",
99
106
  " cricinfo matches scorecard <match>",
107
+ " cricinfo matches live-view <match>",
100
108
  " cricinfo matches innings <match>",
101
109
  }, "\n"),
102
110
  Args: cobra.NoArgs,
@@ -111,6 +119,26 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
111
119
  }
112
120
  liveCmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of live matches to return")
113
121
 
122
+ lineupCmd := &cobra.Command{
123
+ Use: "lineup <match>",
124
+ Short: "Show starting lineups for both teams in one match",
125
+ Long: strings.Join([]string{
126
+ "Resolve one match and return the match-scoped roster entries for both teams.",
127
+ "",
128
+ "Next steps:",
129
+ " cricinfo matches scorecard <match>",
130
+ " cricinfo matches status <match>",
131
+ " cricinfo matches deliveries <match> --team <team> --innings <n> --period <n>",
132
+ }, "\n"),
133
+ Args: cobra.MinimumNArgs(1),
134
+ RunE: func(cmd *cobra.Command, args []string) error {
135
+ query := strings.TrimSpace(strings.Join(args, " "))
136
+ return runMatchCommand(cmd, global, func(ctx context.Context, service matchCommandService) (cricinfo.NormalizedResult, error) {
137
+ return service.Lineups(ctx, query, cricinfo.MatchLookupOptions{LeagueID: opts.leagueID})
138
+ })
139
+ },
140
+ }
141
+
114
142
  showCmd := &cobra.Command{
115
143
  Use: "show <match>",
116
144
  Short: "Show one match summary",
@@ -120,6 +148,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
120
148
  "Next steps:",
121
149
  " cricinfo matches status <match>",
122
150
  " cricinfo matches scorecard <match>",
151
+ " cricinfo matches live-view <match>",
123
152
  " cricinfo matches innings <match>",
124
153
  }, "\n"),
125
154
  Args: cobra.MinimumNArgs(1),
@@ -140,6 +169,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
140
169
  "Next steps:",
141
170
  " cricinfo matches show <match>",
142
171
  " cricinfo matches scorecard <match>",
172
+ " cricinfo matches live-view <match>",
143
173
  " cricinfo matches innings <match>",
144
174
  }, "\n"),
145
175
  Args: cobra.MinimumNArgs(1),
@@ -160,6 +190,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
160
190
  "Next steps:",
161
191
  " cricinfo matches details <match>",
162
192
  " cricinfo matches plays <match>",
193
+ " cricinfo matches live-view <match>",
163
194
  " cricinfo matches situation <match>",
164
195
  }, "\n"),
165
196
  Args: cobra.MinimumNArgs(1),
@@ -200,6 +231,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
200
231
  "Next steps:",
201
232
  " cricinfo matches details <match>",
202
233
  " cricinfo matches scorecard <match>",
234
+ " cricinfo matches live-view <match>",
203
235
  " cricinfo matches situation <match>",
204
236
  }, "\n"),
205
237
  Args: cobra.MinimumNArgs(1),
@@ -220,6 +252,7 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
220
252
  "Next steps:",
221
253
  " cricinfo matches status <match>",
222
254
  " cricinfo matches details <match>",
255
+ " cricinfo matches live-view <match>",
223
256
  " cricinfo matches scorecard <match>",
224
257
  }, "\n"),
225
258
  Args: cobra.MinimumNArgs(1),
@@ -231,6 +264,66 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
231
264
  },
232
265
  }
233
266
 
267
+ liveViewCmd := &cobra.Command{
268
+ Use: "live-view <match>",
269
+ Short: "Show fan-first live snapshot (batters, bowlers, figures, recent balls)",
270
+ Long: strings.Join([]string{
271
+ "Resolve a match and render a synthesized live snapshot from delivery details and roster metadata.",
272
+ "",
273
+ "Shows:",
274
+ " - current batters with runs/balls/strike-rate/boundaries",
275
+ " - current bowlers with figures and economy",
276
+ " - recent balls with over.ball and running score",
277
+ "",
278
+ "Next steps:",
279
+ " cricinfo matches plays <match>",
280
+ " cricinfo matches scorecard <match>",
281
+ " cricinfo matches situation <match>",
282
+ }, "\n"),
283
+ Args: cobra.MinimumNArgs(1),
284
+ RunE: func(cmd *cobra.Command, args []string) error {
285
+ query := strings.TrimSpace(strings.Join(args, " "))
286
+ return runMatchCommand(cmd, global, func(ctx context.Context, service matchCommandService) (cricinfo.NormalizedResult, error) {
287
+ return service.LiveView(ctx, query, cricinfo.MatchLookupOptions{LeagueID: opts.leagueID})
288
+ })
289
+ },
290
+ }
291
+
292
+ duelCmd := &cobra.Command{
293
+ Use: "duel <match>",
294
+ Short: "Show batter-vs-bowler matchup summary in one match",
295
+ Long: strings.Join([]string{
296
+ "Resolve a match and summarize the head-to-head duel between one batter and one bowler.",
297
+ "",
298
+ "Required flags:",
299
+ " --batter <player>",
300
+ " --bowler <player>",
301
+ "",
302
+ "Next steps:",
303
+ " cricinfo matches plays <match>",
304
+ " cricinfo players deliveries <player> --match <match>",
305
+ }, "\n"),
306
+ Args: cobra.MinimumNArgs(1),
307
+ RunE: func(cmd *cobra.Command, args []string) error {
308
+ if strings.TrimSpace(opts.batter) == "" {
309
+ return fmt.Errorf("--batter is required")
310
+ }
311
+ if strings.TrimSpace(opts.bowler) == "" {
312
+ return fmt.Errorf("--bowler is required")
313
+ }
314
+ query := strings.TrimSpace(strings.Join(args, " "))
315
+ return runMatchCommand(cmd, global, func(ctx context.Context, service matchCommandService) (cricinfo.NormalizedResult, error) {
316
+ return service.Duel(ctx, query, cricinfo.MatchDuelOptions{
317
+ LeagueID: opts.leagueID,
318
+ BatterQuery: opts.batter,
319
+ BowlerQuery: opts.bowler,
320
+ })
321
+ })
322
+ },
323
+ }
324
+ duelCmd.Flags().StringVar(&opts.batter, "batter", "", "Required: batter ID/ref/alias")
325
+ duelCmd.Flags().StringVar(&opts.bowler, "bowler", "", "Required: bowler ID/ref/alias")
326
+
234
327
  phasesCmd := &cobra.Command{
235
328
  Use: "phases <match>",
236
329
  Short: "Show powerplay, middle, and death-over phase splits for each innings",
@@ -390,12 +483,15 @@ func newMatchesCommand(global *globalOptions) *cobra.Command {
390
483
  cmd.AddCommand(
391
484
  liveCmd,
392
485
  listCmd,
486
+ lineupCmd,
393
487
  showCmd,
394
488
  statusCmd,
395
489
  scorecardCmd,
396
490
  detailsCmd,
397
491
  playsCmd,
398
492
  situationCmd,
493
+ liveViewCmd,
494
+ duelCmd,
399
495
  phasesCmd,
400
496
  inningsCmd,
401
497
  partnershipsCmd,
@@ -13,12 +13,15 @@ import (
13
13
  type fakeMatchService struct {
14
14
  listResult cricinfo.NormalizedResult
15
15
  liveResult cricinfo.NormalizedResult
16
+ lineupResult cricinfo.NormalizedResult
16
17
  showResult cricinfo.NormalizedResult
17
18
  statusResult cricinfo.NormalizedResult
18
19
  scorecardResult cricinfo.NormalizedResult
19
20
  detailsResult cricinfo.NormalizedResult
20
21
  playsResult cricinfo.NormalizedResult
21
22
  situationResult cricinfo.NormalizedResult
23
+ liveViewResult cricinfo.NormalizedResult
24
+ duelResult cricinfo.NormalizedResult
22
25
  phasesResult cricinfo.NormalizedResult
23
26
  inningsResult cricinfo.NormalizedResult
24
27
  partnerships cricinfo.NormalizedResult
@@ -45,6 +48,10 @@ func (f *fakeMatchService) Live(context.Context, cricinfo.MatchListOptions) (cri
45
48
  return f.liveResult, nil
46
49
  }
47
50
 
51
+ func (f *fakeMatchService) Lineups(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
52
+ return f.lineupResult, nil
53
+ }
54
+
48
55
  func (f *fakeMatchService) Show(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
49
56
  return f.showResult, nil
50
57
  }
@@ -69,6 +76,14 @@ func (f *fakeMatchService) Situation(context.Context, string, cricinfo.MatchLook
69
76
  return f.situationResult, nil
70
77
  }
71
78
 
79
+ func (f *fakeMatchService) LiveView(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
80
+ return f.liveViewResult, nil
81
+ }
82
+
83
+ func (f *fakeMatchService) Duel(context.Context, string, cricinfo.MatchDuelOptions) (cricinfo.NormalizedResult, error) {
84
+ return f.duelResult, nil
85
+ }
86
+
72
87
  func (f *fakeMatchService) Phases(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
73
88
  return f.phasesResult, nil
74
89
  }
@@ -149,12 +164,39 @@ func TestMatchesCommandsRenderTextAndJSON(t *testing.T) {
149
164
  service := &fakeMatchService{
150
165
  listResult: cricinfo.NewListResult(cricinfo.EntityMatch, []any{match}),
151
166
  liveResult: cricinfo.NewListResult(cricinfo.EntityMatch, []any{match}),
167
+ lineupResult: cricinfo.NewListResult(cricinfo.EntityTeamRoster, []any{cricinfo.TeamRosterEntry{DisplayName: "Player One", TeamName: "BOOST"}}),
152
168
  showResult: cricinfo.NewDataResult(cricinfo.EntityMatch, match),
153
169
  statusResult: cricinfo.NewDataResult(cricinfo.EntityMatch, match),
154
170
  scorecardResult: cricinfo.NewDataResult(cricinfo.EntityMatchScorecard, scorecard),
155
171
  detailsResult: cricinfo.NewListResult(cricinfo.EntityDeliveryEvent, []any{delivery}),
156
172
  playsResult: cricinfo.NewListResult(cricinfo.EntityDeliveryEvent, []any{delivery}),
157
173
  situationResult: cricinfo.NewDataResult(cricinfo.EntityMatchSituation, situation),
174
+ liveViewResult: cricinfo.NewDataResult(cricinfo.EntityMatchSituation, cricinfo.MatchSituation{
175
+ MatchID: "1529474",
176
+ Live: &cricinfo.MatchLiveView{
177
+ Score: "69/2",
178
+ Batters: []cricinfo.LiveBatterView{
179
+ {PlayerName: "Numan Shah", Runs: 52, Balls: 41, StrikeRate: 126.82, OnStrike: true},
180
+ },
181
+ Bowlers: []cricinfo.LiveBowlerView{
182
+ {PlayerName: "Hayatullah Noori", Overs: 3, Conceded: 21, Wickets: 1, Economy: 7.0},
183
+ },
184
+ RecentBalls: []cricinfo.DeliveryEvent{delivery},
185
+ },
186
+ }),
187
+ duelResult: cricinfo.NewDataResult(cricinfo.EntityMatchDuel, cricinfo.MatchDuel{
188
+ MatchID: "1529474",
189
+ BatterName: "Numan Shah",
190
+ BowlerName: "Hayatullah Noori",
191
+ Runs: 10,
192
+ Balls: 7,
193
+ Dots: 2,
194
+ Fours: 2,
195
+ StrikeRate: 142.86,
196
+ RecentBalls: []cricinfo.DeliveryEvent{
197
+ delivery,
198
+ },
199
+ }),
158
200
  phasesResult: cricinfo.NewDataResult(cricinfo.EntityMatchPhases, cricinfo.MatchPhases{
159
201
  MatchID: "1529474",
160
202
  Innings: []cricinfo.MatchPhaseInning{
@@ -296,6 +338,15 @@ func TestMatchesCommandsRenderTextAndJSON(t *testing.T) {
296
338
  t.Fatalf("expected plays text output to include normalized short text, got %q", playsOut.String())
297
339
  }
298
340
 
341
+ var lineupOut bytes.Buffer
342
+ var lineupErr bytes.Buffer
343
+ if err := Run([]string{"matches", "lineup", "1529474", "--format", "text"}, &lineupOut, &lineupErr); err != nil {
344
+ t.Fatalf("Run matches lineup --format text error: %v", err)
345
+ }
346
+ if !strings.Contains(lineupOut.String(), "Player One") {
347
+ t.Fatalf("expected lineup text to include roster names, got %q", lineupOut.String())
348
+ }
349
+
299
350
  var situationOut bytes.Buffer
300
351
  var situationErr bytes.Buffer
301
352
  if err := Run([]string{"matches", "situation", "1529474", "--format", "json"}, &situationOut, &situationErr); err != nil {
@@ -309,6 +360,26 @@ func TestMatchesCommandsRenderTextAndJSON(t *testing.T) {
309
360
  t.Fatalf("expected kind %q in situation output, got %#v", cricinfo.EntityMatchSituation, situationPayload["kind"])
310
361
  }
311
362
 
363
+ var liveViewOut bytes.Buffer
364
+ var liveViewErr bytes.Buffer
365
+ if err := Run([]string{"matches", "live-view", "1529474", "--format", "text"}, &liveViewOut, &liveViewErr); err != nil {
366
+ t.Fatalf("Run matches live-view --format text error: %v", err)
367
+ }
368
+ liveViewText := liveViewOut.String()
369
+ if !strings.Contains(liveViewText, "Batters") || !strings.Contains(liveViewText, "Bowlers") {
370
+ t.Fatalf("expected batters/bowlers sections in live-view output, got %q", liveViewText)
371
+ }
372
+
373
+ var duelOut bytes.Buffer
374
+ var duelErr bytes.Buffer
375
+ if err := Run([]string{"matches", "duel", "1529474", "--batter", "Numan Shah", "--bowler", "Hayatullah Noori", "--format", "text"}, &duelOut, &duelErr); err != nil {
376
+ t.Fatalf("Run matches duel --format text error: %v", err)
377
+ }
378
+ duelText := duelOut.String()
379
+ if !strings.Contains(duelText, "Duel:") || !strings.Contains(duelText, "Numan Shah") {
380
+ t.Fatalf("expected duel output to include matchup summary, got %q", duelText)
381
+ }
382
+
312
383
  var inningsOut bytes.Buffer
313
384
  var inningsErr bytes.Buffer
314
385
  if err := Run([]string{"matches", "innings", "1529474", "--team", "BOOST", "--format", "text"}, &inningsOut, &inningsErr); err != nil {
@@ -95,6 +95,13 @@ func newSearchEntityCommand(name string, kind cricinfo.EntityKind, global *globa
95
95
  for _, entity := range searchResult.Entities {
96
96
  items = append(items, entity.ToRenderable())
97
97
  }
98
+ if kind == cricinfo.EntityMatch && len(items) == 0 && query != "" {
99
+ fallbackItems, fallbackWarnings := fallbackMatchSearchItems(cmd.Context(), query, searchOpts.leagueID, searchOpts.limit)
100
+ if len(fallbackItems) > 0 {
101
+ items = fallbackItems
102
+ }
103
+ searchResult.Warnings = append(searchResult.Warnings, fallbackWarnings...)
104
+ }
98
105
 
99
106
  result := cricinfo.NewListResult(kind, items)
100
107
  if len(searchResult.Warnings) > 0 {
@@ -117,3 +124,152 @@ func newSearchEntityCommand(name string, kind cricinfo.EntityKind, global *globa
117
124
 
118
125
  return command
119
126
  }
127
+
128
+ func fallbackMatchSearchItems(ctx context.Context, query string, leagueID string, limit int) ([]any, []string) {
129
+ service, err := cricinfo.NewMatchService(cricinfo.MatchServiceConfig{})
130
+ if err != nil {
131
+ return nil, []string{fmt.Sprintf("match fallback init failed: %v", err)}
132
+ }
133
+ defer func() {
134
+ _ = service.Close()
135
+ }()
136
+
137
+ listLimit := limit
138
+ if listLimit < 20 {
139
+ listLimit = 20
140
+ }
141
+ if listLimit > 80 {
142
+ listLimit = 80
143
+ }
144
+
145
+ result, err := service.Live(ctx, cricinfo.MatchListOptions{Limit: listLimit, LeagueID: strings.TrimSpace(leagueID)})
146
+ if err != nil {
147
+ return nil, []string{fmt.Sprintf("match fallback live list failed: %v", err)}
148
+ }
149
+ if len(result.Items) == 0 {
150
+ result, err = service.List(ctx, cricinfo.MatchListOptions{Limit: listLimit, LeagueID: strings.TrimSpace(leagueID)})
151
+ if err != nil {
152
+ return nil, []string{fmt.Sprintf("match fallback list failed: %v", err)}
153
+ }
154
+ }
155
+
156
+ queryNorm := normalizeMatchSearchText(query)
157
+ queryTokens := strings.Fields(queryNorm)
158
+ if queryNorm == "" {
159
+ return nil, nil
160
+ }
161
+
162
+ items := make([]any, 0, limitOrDefault(limit, 10))
163
+ for _, raw := range result.Items {
164
+ match, ok := raw.(cricinfo.Match)
165
+ if !ok {
166
+ continue
167
+ }
168
+ if matchSearchScore(match, queryNorm, queryTokens) == 0 {
169
+ continue
170
+ }
171
+ items = append(items, match)
172
+ if len(items) >= limitOrDefault(limit, 10) {
173
+ break
174
+ }
175
+ }
176
+
177
+ if len(items) == 0 {
178
+ return nil, nil
179
+ }
180
+ return items, []string{"match search fallback used list/live aliases"}
181
+ }
182
+
183
+ func matchSearchScore(match cricinfo.Match, query string, queryTokens []string) int {
184
+ score := 0
185
+ for _, value := range []string{
186
+ match.ID,
187
+ match.Description,
188
+ match.ShortDescription,
189
+ match.Note,
190
+ match.ScoreSummary,
191
+ } {
192
+ score = maxInt(score, aliasScore(normalizeMatchSearchText(value), query, queryTokens))
193
+ }
194
+ for _, team := range match.Teams {
195
+ for _, value := range []string{
196
+ team.ID,
197
+ team.Name,
198
+ team.ShortName,
199
+ team.Abbreviation,
200
+ } {
201
+ score = maxInt(score, aliasScore(normalizeMatchSearchText(value), query, queryTokens))
202
+ }
203
+ }
204
+ return score
205
+ }
206
+
207
+ func aliasScore(alias, query string, queryTokens []string) int {
208
+ if alias == "" || query == "" {
209
+ return 0
210
+ }
211
+ if alias == query {
212
+ return 1000
213
+ }
214
+ if strings.HasPrefix(alias, query) {
215
+ return 800
216
+ }
217
+ if strings.Contains(alias, query) {
218
+ return 650
219
+ }
220
+ aliasTokens := strings.Fields(alias)
221
+ if len(aliasTokens) == 0 || len(queryTokens) == 0 {
222
+ return 0
223
+ }
224
+ matched := 0
225
+ for _, queryToken := range queryTokens {
226
+ for _, aliasToken := range aliasTokens {
227
+ if aliasToken == queryToken || strings.HasPrefix(aliasToken, queryToken) || (len(aliasToken) >= 2 && strings.HasPrefix(queryToken, aliasToken)) {
228
+ matched++
229
+ break
230
+ }
231
+ }
232
+ }
233
+ if matched == 0 {
234
+ return 0
235
+ }
236
+ return 300 + (matched * 60)
237
+ }
238
+
239
+ func normalizeMatchSearchText(raw string) string {
240
+ raw = strings.ToLower(strings.TrimSpace(raw))
241
+ if raw == "" {
242
+ return ""
243
+ }
244
+ var builder strings.Builder
245
+ lastSpace := false
246
+ for _, r := range raw {
247
+ if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
248
+ builder.WriteRune(r)
249
+ lastSpace = false
250
+ continue
251
+ }
252
+ if !lastSpace {
253
+ builder.WriteRune(' ')
254
+ lastSpace = true
255
+ }
256
+ }
257
+ return strings.Join(strings.Fields(builder.String()), " ")
258
+ }
259
+
260
+ func maxInt(a, b int) int {
261
+ if a > b {
262
+ return a
263
+ }
264
+ return b
265
+ }
266
+
267
+ func limitOrDefault(value int, fallback int) int {
268
+ if value > 0 {
269
+ return value
270
+ }
271
+ if fallback <= 0 {
272
+ return 10
273
+ }
274
+ return fallback
275
+ }