@stats-forge/github-stats-forge-core 0.0.1

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/build/api/api-result.d.ts +44 -0
  4. package/build/api/api-result.d.ts.map +1 -0
  5. package/build/api/api-result.js +33 -0
  6. package/build/api/gist.d.ts +29 -0
  7. package/build/api/gist.d.ts.map +1 -0
  8. package/build/api/gist.js +52 -0
  9. package/build/api/index.d.ts +13 -0
  10. package/build/api/index.d.ts.map +1 -0
  11. package/build/api/index.js +8 -0
  12. package/build/api/params.d.ts +93 -0
  13. package/build/api/params.d.ts.map +1 -0
  14. package/build/api/params.js +145 -0
  15. package/build/api/pin.d.ts +47 -0
  16. package/build/api/pin.d.ts.map +1 -0
  17. package/build/api/pin.js +87 -0
  18. package/build/api/stats.d.ts +42 -0
  19. package/build/api/stats.d.ts.map +1 -0
  20. package/build/api/stats.js +132 -0
  21. package/build/api/top-langs.d.ts +37 -0
  22. package/build/api/top-langs.d.ts.map +1 -0
  23. package/build/api/top-langs.js +101 -0
  24. package/build/api/wakatime.d.ts +34 -0
  25. package/build/api/wakatime.d.ts.map +1 -0
  26. package/build/api/wakatime.js +91 -0
  27. package/build/calculateRank.d.ts +29 -0
  28. package/build/calculateRank.d.ts.map +1 -0
  29. package/build/calculateRank.js +58 -0
  30. package/build/cards/gist.d.ts +16 -0
  31. package/build/cards/gist.d.ts.map +1 -0
  32. package/build/cards/gist.js +119 -0
  33. package/build/cards/index.d.ts +11 -0
  34. package/build/cards/index.d.ts.map +1 -0
  35. package/build/cards/index.js +7 -0
  36. package/build/cards/options.d.ts +19 -0
  37. package/build/cards/options.d.ts.map +1 -0
  38. package/build/cards/options.js +1 -0
  39. package/build/cards/repo.d.ts +25 -0
  40. package/build/cards/repo.d.ts.map +1 -0
  41. package/build/cards/repo.js +276 -0
  42. package/build/cards/stats.d.ts +37 -0
  43. package/build/cards/stats.d.ts.map +1 -0
  44. package/build/cards/stats.js +457 -0
  45. package/build/cards/top-languages.d.ts +153 -0
  46. package/build/cards/top-languages.d.ts.map +1 -0
  47. package/build/cards/top-languages.js +805 -0
  48. package/build/cards/wakatime.d.ts +31 -0
  49. package/build/cards/wakatime.d.ts.map +1 -0
  50. package/build/cards/wakatime.js +376 -0
  51. package/build/common/Card.d.ts +121 -0
  52. package/build/common/Card.d.ts.map +1 -0
  53. package/build/common/Card.js +305 -0
  54. package/build/common/I18n.d.ts +27 -0
  55. package/build/common/I18n.d.ts.map +1 -0
  56. package/build/common/I18n.js +35 -0
  57. package/build/common/color.d.ts +112 -0
  58. package/build/common/color.d.ts.map +1 -0
  59. package/build/common/color.js +188 -0
  60. package/build/common/config.d.ts +53 -0
  61. package/build/common/config.d.ts.map +1 -0
  62. package/build/common/config.js +87 -0
  63. package/build/common/constants.d.ts +7 -0
  64. package/build/common/constants.d.ts.map +1 -0
  65. package/build/common/constants.js +9 -0
  66. package/build/common/date.d.ts +29 -0
  67. package/build/common/date.d.ts.map +1 -0
  68. package/build/common/date.js +23 -0
  69. package/build/common/error.d.ts +87 -0
  70. package/build/common/error.d.ts.map +1 -0
  71. package/build/common/error.js +108 -0
  72. package/build/common/fmt.d.ts +28 -0
  73. package/build/common/fmt.d.ts.map +1 -0
  74. package/build/common/fmt.js +68 -0
  75. package/build/common/html.d.ts +8 -0
  76. package/build/common/html.d.ts.map +1 -0
  77. package/build/common/html.js +14 -0
  78. package/build/common/http.d.ts +53 -0
  79. package/build/common/http.d.ts.map +1 -0
  80. package/build/common/http.js +47 -0
  81. package/build/common/icons.d.ts +28 -0
  82. package/build/common/icons.d.ts.map +1 -0
  83. package/build/common/icons.js +77 -0
  84. package/build/common/languageColors.d.ts +11 -0
  85. package/build/common/languageColors.d.ts.map +1 -0
  86. package/build/common/languageColors.js +15 -0
  87. package/build/common/languageColors.json +678 -0
  88. package/build/common/log.d.ts +9 -0
  89. package/build/common/log.d.ts.map +1 -0
  90. package/build/common/log.js +8 -0
  91. package/build/common/ops.d.ts +59 -0
  92. package/build/common/ops.d.ts.map +1 -0
  93. package/build/common/ops.js +115 -0
  94. package/build/common/render.d.ts +196 -0
  95. package/build/common/render.d.ts.map +1 -0
  96. package/build/common/render.js +474 -0
  97. package/build/common/retryer.d.ts +36 -0
  98. package/build/common/retryer.d.ts.map +1 -0
  99. package/build/common/retryer.js +67 -0
  100. package/build/fetchers/gist.d.ts +15 -0
  101. package/build/fetchers/gist.d.ts.map +1 -0
  102. package/build/fetchers/gist.js +68 -0
  103. package/build/fetchers/index.d.ts +13 -0
  104. package/build/fetchers/index.d.ts.map +1 -0
  105. package/build/fetchers/index.js +10 -0
  106. package/build/fetchers/repo.d.ts +27 -0
  107. package/build/fetchers/repo.d.ts.map +1 -0
  108. package/build/fetchers/repo.js +100 -0
  109. package/build/fetchers/stats.d.ts +74 -0
  110. package/build/fetchers/stats.d.ts.map +1 -0
  111. package/build/fetchers/stats.js +498 -0
  112. package/build/fetchers/top-languages.d.ts +23 -0
  113. package/build/fetchers/top-languages.d.ts.map +1 -0
  114. package/build/fetchers/top-languages.js +89 -0
  115. package/build/fetchers/types.d.ts +130 -0
  116. package/build/fetchers/types.d.ts.map +1 -0
  117. package/build/fetchers/types.js +1 -0
  118. package/build/fetchers/wakatime.d.ts +19 -0
  119. package/build/fetchers/wakatime.d.ts.map +1 -0
  120. package/build/fetchers/wakatime.js +36 -0
  121. package/build/graphql/contributionsDocument.d.ts +18 -0
  122. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  123. package/build/graphql/contributionsDocument.js +30 -0
  124. package/build/graphql/generated/common.d.ts +42 -0
  125. package/build/graphql/generated/common.d.ts.map +1 -0
  126. package/build/graphql/generated/common.js +2 -0
  127. package/build/graphql/generated/gist.d.ts +44 -0
  128. package/build/graphql/generated/gist.d.ts.map +1 -0
  129. package/build/graphql/generated/gist.js +27 -0
  130. package/build/graphql/generated/repo.d.ts +67 -0
  131. package/build/graphql/generated/repo.d.ts.map +1 -0
  132. package/build/graphql/generated/repo.js +30 -0
  133. package/build/graphql/generated/stats.d.ts +150 -0
  134. package/build/graphql/generated/stats.d.ts.map +1 -0
  135. package/build/graphql/generated/stats.js +95 -0
  136. package/build/graphql/generated/top-languages.d.ts +54 -0
  137. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  138. package/build/graphql/generated/top-languages.js +27 -0
  139. package/build/graphql/graphqlDocument.d.ts +22 -0
  140. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  141. package/build/graphql/graphqlDocument.js +8 -0
  142. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  143. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  144. package/build/graphql/reposContributedToDocument.js +59 -0
  145. package/build/index.d.ts +15 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +10 -0
  148. package/build/themes/index.d.ts +505 -0
  149. package/build/themes/index.d.ts.map +1 -0
  150. package/build/themes/index.js +505 -0
  151. package/build/translations.d.ts +1153 -0
  152. package/build/translations.d.ts.map +1 -0
  153. package/build/translations.js +1156 -0
  154. package/package.json +114 -0
  155. package/src/_emoji-name-map.d.ts +10 -0
  156. package/src/_github-username-regex.d.ts +4 -0
  157. package/src/api/api-result.ts +69 -0
  158. package/src/api/gist.ts +66 -0
  159. package/src/api/index.ts +16 -0
  160. package/src/api/params.ts +221 -0
  161. package/src/api/pin.ts +126 -0
  162. package/src/api/stats.ts +195 -0
  163. package/src/api/top-langs.ts +150 -0
  164. package/src/api/wakatime.ts +130 -0
  165. package/src/calculateRank.ts +91 -0
  166. package/src/cards/gist.ts +169 -0
  167. package/src/cards/index.ts +21 -0
  168. package/src/cards/options.ts +18 -0
  169. package/src/cards/repo.ts +376 -0
  170. package/src/cards/stats.ts +615 -0
  171. package/src/cards/top-languages.ts +1077 -0
  172. package/src/cards/wakatime.ts +501 -0
  173. package/src/common/Card.ts +364 -0
  174. package/src/common/I18n.ts +51 -0
  175. package/src/common/color.ts +296 -0
  176. package/src/common/config.ts +118 -0
  177. package/src/common/constants.ts +12 -0
  178. package/src/common/date.ts +34 -0
  179. package/src/common/error.ts +163 -0
  180. package/src/common/fmt.ts +86 -0
  181. package/src/common/html.ts +17 -0
  182. package/src/common/http.ts +98 -0
  183. package/src/common/icons.ts +80 -0
  184. package/src/common/languageColors.json +678 -0
  185. package/src/common/languageColors.ts +19 -0
  186. package/src/common/log.ts +12 -0
  187. package/src/common/ops.ts +145 -0
  188. package/src/common/render.ts +641 -0
  189. package/src/common/retryer.ts +111 -0
  190. package/src/fetchers/gist.ts +82 -0
  191. package/src/fetchers/index.ts +25 -0
  192. package/src/fetchers/repo.ts +140 -0
  193. package/src/fetchers/stats.ts +742 -0
  194. package/src/fetchers/top-languages.ts +134 -0
  195. package/src/fetchers/types.ts +136 -0
  196. package/src/fetchers/wakatime.ts +56 -0
  197. package/src/graphql/contributionsDocument.ts +44 -0
  198. package/src/graphql/generated/common.ts +26 -0
  199. package/src/graphql/generated/gist.ts +56 -0
  200. package/src/graphql/generated/repo.ts +79 -0
  201. package/src/graphql/generated/stats.ts +180 -0
  202. package/src/graphql/generated/top-languages.ts +66 -0
  203. package/src/graphql/graphqlDocument.ts +25 -0
  204. package/src/graphql/queries/gist.graphql +25 -0
  205. package/src/graphql/queries/repo.graphql +28 -0
  206. package/src/graphql/queries/stats.graphql +113 -0
  207. package/src/graphql/queries/top-languages.graphql +27 -0
  208. package/src/graphql/reposContributedToDocument.ts +83 -0
  209. package/src/index.ts +18 -0
  210. package/src/themes/index.ts +519 -0
  211. package/src/translations.ts +1169 -0
@@ -0,0 +1,615 @@
1
+ import { Card } from '../common/Card.js';
2
+ import { getLightDarkColors } from '../common/color.js';
3
+ import type { CardColors } from '../common/color.js';
4
+ import { CardError } from '../common/error.js';
5
+ import { I18n } from '../common/I18n.js';
6
+ import { icons, rankIcon } from '../common/icons.js';
7
+ import { buildSearchFilter, clampValue } from '../common/ops.js';
8
+ import { createTextNode, flexLayout, measureText } from '../common/render.js';
9
+ import type { StatsData } from '../fetchers/types.js';
10
+ import { statCardLocales, wakatimeCardLocales } from '../translations.js';
11
+
12
+ import type { CardOptions, CommonCardOptions } from './options.js';
13
+
14
+ const CARD_MIN_WIDTH = 287;
15
+ const CARD_DEFAULT_WIDTH = 287;
16
+ const RANK_CARD_MIN_WIDTH = 420;
17
+ const RANK_CARD_DEFAULT_WIDTH = 450;
18
+ const RANK_ONLY_CARD_MIN_WIDTH = 290;
19
+ const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
20
+
21
+ /** Rank indicators the card can draw; the api validates `rank_icon` against this. */
22
+ const RANK_ICONS = ['default', 'github', 'percentile'] as const;
23
+ type RankIcon = (typeof RANK_ICONS)[number];
24
+
25
+ interface StatCardOptions extends CommonCardOptions {
26
+ locale: string;
27
+ hide: Array<string>;
28
+ show_icons: boolean;
29
+ hide_title: boolean;
30
+ card_width: number;
31
+ hide_rank: boolean;
32
+ include_all_commits: boolean;
33
+ commits_year: number;
34
+ line_height: number | string;
35
+ custom_title: string;
36
+ disable_animations: boolean;
37
+ number_format: string;
38
+ number_precision: number;
39
+ ring_color: string;
40
+ text_bold: boolean;
41
+ rank_icon: RankIcon;
42
+ show: Array<string>;
43
+ }
44
+
45
+ /** Meta data for a stat, used to build its text node and accessibility label. */
46
+ interface StatItem {
47
+ icon: string;
48
+ label: string;
49
+ value: number | string;
50
+ id: string;
51
+ unitSymbol?: string;
52
+ link?: string;
53
+ }
54
+
55
+ /** Long locales that need more space for text. Keep sorted alphabetically. */
56
+ const LONG_LOCALES: Array<string> = [
57
+ 'az',
58
+ 'bg',
59
+ 'cs',
60
+ 'de',
61
+ 'el',
62
+ 'es',
63
+ 'fil',
64
+ 'fi',
65
+ 'fr',
66
+ 'hu',
67
+ 'id',
68
+ 'ja',
69
+ 'ml',
70
+ 'my',
71
+ 'nl',
72
+ 'pl',
73
+ 'pt-br',
74
+ 'pt-pt',
75
+ 'ru',
76
+ 'sr',
77
+ 'sr-latn',
78
+ 'sw',
79
+ 'ta',
80
+ 'uk-ua',
81
+ 'uz',
82
+ 'zh-tw',
83
+ ];
84
+
85
+ /**
86
+ * Calculates progress along the boundary of the circle, i.e. its circumference.
87
+ *
88
+ * @param value The rank value to calculate progress for.
89
+ * @returns Progress value.
90
+ */
91
+ const calculateCircleProgress = (value: number): number => {
92
+ const radius = 40;
93
+ const c = Math.PI * (radius * 2);
94
+
95
+ if (value < 0) {
96
+ value = 0;
97
+ }
98
+ if (value > 100) {
99
+ value = 100;
100
+ }
101
+
102
+ return ((100 - value) / 100) * c;
103
+ };
104
+
105
+ /**
106
+ * Retrieves the animation to display progress along the circumference of circle
107
+ * from the beginning to the given value in a clockwise direction.
108
+ *
109
+ * @param props The props object.
110
+ * @param props.progress The progress value to animate to.
111
+ * @returns Progress animation css.
112
+ */
113
+ const getProgressAnimation = ({ progress }: { progress: number }): string => {
114
+ return `
115
+ @keyframes rankAnimation {
116
+ from {
117
+ stroke-dashoffset: ${calculateCircleProgress(0)};
118
+ }
119
+ to {
120
+ stroke-dashoffset: ${calculateCircleProgress(progress)};
121
+ }
122
+ }
123
+ `;
124
+ };
125
+
126
+ /**
127
+ * Retrieves CSS styles for a card.
128
+ *
129
+ * @param colors The colors to use for the card.
130
+ * @param colors.textColor The text color.
131
+ * @param colors.iconColor The icon color.
132
+ * @param colors.ringColor The ring color.
133
+ * @param colors.show_icons Whether to show icons.
134
+ * @param colors.progress The progress value to animate to.
135
+ * @returns Card CSS styles.
136
+ */
137
+ const getStyles = ({
138
+ textColor,
139
+ iconColor,
140
+ ringColor,
141
+ show_icons,
142
+ progress,
143
+ }: {
144
+ textColor: string;
145
+ iconColor: string;
146
+ ringColor: string;
147
+ show_icons: boolean;
148
+ progress: number;
149
+ }): string => {
150
+ return `
151
+ .stat {
152
+ font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
153
+ }
154
+ @supports(-moz-appearance: auto) {
155
+ /* Selector detects Firefox */
156
+ .stat { font-size:12px; }
157
+ }
158
+ .stagger {
159
+ opacity: 0;
160
+ animation: fadeInAnimation 0.3s ease-in-out forwards;
161
+ }
162
+ .rank-text {
163
+ font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
164
+ animation: scaleInAnimation 0.3s ease-in-out forwards;
165
+ }
166
+ .rank-percentile-header {
167
+ font-size: 14px;
168
+ }
169
+ .rank-percentile-text {
170
+ font-size: 16px;
171
+ }
172
+
173
+ .not_bold { font-weight: 400 }
174
+ .bold { font-weight: 700 }
175
+ .icon {
176
+ fill: ${iconColor};
177
+ display: ${show_icons ? 'block' : 'none'};
178
+ }
179
+
180
+ .rank-circle-rim {
181
+ stroke: ${ringColor};
182
+ fill: none;
183
+ stroke-width: 6;
184
+ opacity: 0.2;
185
+ }
186
+ .rank-circle {
187
+ stroke: ${ringColor};
188
+ stroke-dasharray: 250;
189
+ fill: none;
190
+ stroke-width: 6;
191
+ stroke-linecap: round;
192
+ opacity: 0.8;
193
+ transform-origin: -10px 8px;
194
+ transform: rotate(-90deg);
195
+ animation: rankAnimation 1s forwards ease-in-out;
196
+ }
197
+ ${getProgressAnimation({ progress })}
198
+ `;
199
+ };
200
+
201
+ /**
202
+ * Return the label for commits according to the selected options
203
+ *
204
+ * @param include_all_commits Option to include all years
205
+ * @param commits_year Option to include only selected year
206
+ * @param i18n The I18n instance.
207
+ * @returns The label corresponding to the options.
208
+ */
209
+ const getTotalCommitsYearLabel = (
210
+ include_all_commits: boolean,
211
+ commits_year: number | undefined,
212
+ i18n: I18n,
213
+ ): string =>
214
+ include_all_commits
215
+ ? ''
216
+ : commits_year
217
+ ? ` (${commits_year})`
218
+ : ` (${i18n.t('wakatimecard.lastyear')})`;
219
+
220
+ /**
221
+ * Renders the stats card.
222
+ *
223
+ * @param stats The stats data.
224
+ * @param options The card options.
225
+ * @param username GitHub username, used to build stat search links.
226
+ * @param repo Repositories to scope the search links to.
227
+ * @param owner Owners to scope the search links to.
228
+ * @returns The stats card SVG object.
229
+ */
230
+ const renderStatsCard = (
231
+ stats: StatsData,
232
+ options: CardOptions<StatCardOptions> = {},
233
+ username?: string,
234
+ repo: Array<string> = [],
235
+ owner: Array<string> = [],
236
+ ): string => {
237
+ const {
238
+ name,
239
+ totalStars,
240
+ totalCommits,
241
+ totalIssues,
242
+ totalPRs,
243
+ totalPRsMerged,
244
+ mergedPRsPercentage,
245
+ totalReviews,
246
+ totalDiscussionsStarted,
247
+ totalDiscussionsAnswered,
248
+ contributedTo,
249
+ allTimeContributedTo,
250
+ totalPRsAuthored,
251
+ totalPRsCommented,
252
+ totalPRsReviewed,
253
+ totalIssuesAuthored,
254
+ totalIssuesCommented,
255
+ totalContributions,
256
+ rank,
257
+ } = stats;
258
+ const {
259
+ hide = [],
260
+ show_icons = false,
261
+ hide_title = false,
262
+ hide_border = false,
263
+ card_width,
264
+ hide_rank = false,
265
+ include_all_commits = false,
266
+ commits_year,
267
+ line_height = 25,
268
+ text_bold = true,
269
+ custom_title,
270
+ border_radius,
271
+ number_format = 'short',
272
+ number_precision,
273
+ locale,
274
+ disable_animations = false,
275
+ rank_icon = 'default',
276
+ show = [],
277
+ } = options;
278
+
279
+ const lheight = parseInt(String(line_height), 10);
280
+
281
+ const { lightColors, darkColors } = getLightDarkColors(options);
282
+
283
+ const apostrophe = /s$/i.test(name.trim()) ? '' : 's';
284
+ const i18n = new I18n({
285
+ locale,
286
+ translations: {
287
+ ...statCardLocales({ name, apostrophe }),
288
+ ...wakatimeCardLocales,
289
+ },
290
+ });
291
+
292
+ // Meta data for creating text nodes with createTextNode function
293
+ const STATS: Record<string, StatItem> = {};
294
+
295
+ STATS['stars'] = {
296
+ icon: icons.star,
297
+ label: i18n.t('statcard.totalstars'),
298
+ value: totalStars,
299
+ id: 'stars',
300
+ };
301
+
302
+ if (show.includes('contributions')) {
303
+ STATS['contributions'] = {
304
+ icon: icons.contributions,
305
+ label: i18n.t('statcard.contributions'),
306
+ value: totalContributions,
307
+ id: 'contributions',
308
+ };
309
+ }
310
+
311
+ STATS['commits'] = {
312
+ icon: icons.commits,
313
+ label: `${i18n.t('statcard.commits')}${getTotalCommitsYearLabel(
314
+ include_all_commits,
315
+ commits_year,
316
+ i18n,
317
+ )}`,
318
+ value: totalCommits,
319
+ id: 'commits',
320
+ };
321
+ STATS['prs'] = {
322
+ icon: icons.prs,
323
+ label: i18n.t('statcard.prs'),
324
+ value: totalPRs,
325
+ id: 'prs',
326
+ };
327
+
328
+ if (show.includes('prs_merged')) {
329
+ STATS['prs_merged'] = {
330
+ icon: icons.prs_merged,
331
+ label: i18n.t('statcard.prs-merged'),
332
+ value: totalPRsMerged,
333
+ id: 'prs_merged',
334
+ };
335
+ }
336
+
337
+ if (show.includes('prs_merged_percentage')) {
338
+ STATS['prs_merged_percentage'] = {
339
+ icon: icons.prs_merged_percentage,
340
+ label: i18n.t('statcard.prs-merged-percentage'),
341
+ value: mergedPRsPercentage.toFixed(
342
+ number_precision !== undefined && Number.isFinite(number_precision)
343
+ ? clampValue(number_precision, 0, 2)
344
+ : 2,
345
+ ),
346
+ id: 'prs_merged_percentage',
347
+ unitSymbol: '%',
348
+ };
349
+ }
350
+
351
+ if (show.includes('reviews')) {
352
+ STATS['reviews'] = {
353
+ icon: icons.reviews,
354
+ label: i18n.t('statcard.reviews'),
355
+ value: totalReviews,
356
+ id: 'reviews',
357
+ };
358
+ }
359
+
360
+ STATS['issues'] = {
361
+ icon: icons.issues,
362
+ label: i18n.t('statcard.issues'),
363
+ value: totalIssues,
364
+ id: 'issues',
365
+ };
366
+
367
+ if (show.includes('discussions_started')) {
368
+ STATS['discussions_started'] = {
369
+ icon: icons.discussions_started,
370
+ label: i18n.t('statcard.discussions-started'),
371
+ value: totalDiscussionsStarted,
372
+ id: 'discussions_started',
373
+ };
374
+ }
375
+ if (show.includes('discussions_answered')) {
376
+ STATS['discussions_answered'] = {
377
+ icon: icons.discussions_answered,
378
+ label: i18n.t('statcard.discussions-answered'),
379
+ value: totalDiscussionsAnswered,
380
+ id: 'discussions_answered',
381
+ };
382
+ }
383
+
384
+ const repoFilter = encodeURIComponent(buildSearchFilter(repo, owner));
385
+ const encodedUsername = encodeURIComponent(username ?? '');
386
+ if (show.includes('prs_authored')) {
387
+ STATS['prs_authored'] = {
388
+ icon: icons.prs,
389
+ label: i18n.t('statcard.prs-authored'),
390
+ value: totalPRsAuthored,
391
+ id: 'prs_authored',
392
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
393
+ };
394
+ }
395
+ if (show.includes('prs_commented')) {
396
+ STATS['prs_commented'] = {
397
+ icon: icons.comments,
398
+ label: i18n.t('statcard.prs-commented'),
399
+ value: totalPRsCommented,
400
+ id: 'prs_commented',
401
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
402
+ };
403
+ }
404
+ if (show.includes('prs_reviewed')) {
405
+ STATS['prs_reviewed'] = {
406
+ icon: icons.reviews,
407
+ label: i18n.t('statcard.prs-reviewed'),
408
+ value: totalPRsReviewed,
409
+ id: 'prs_reviewed',
410
+ link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
411
+ };
412
+ }
413
+ if (show.includes('issues_authored')) {
414
+ STATS['issues_authored'] = {
415
+ icon: icons.issues,
416
+ label: i18n.t('statcard.issues-authored'),
417
+ value: totalIssuesAuthored,
418
+ id: 'issues_authored',
419
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
420
+ };
421
+ }
422
+ if (show.includes('issues_commented')) {
423
+ STATS['issues_commented'] = {
424
+ icon: icons.discussions_started,
425
+ label: i18n.t('statcard.issues-commented'),
426
+ value: totalIssuesCommented,
427
+ id: 'issues_commented',
428
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=issues`,
429
+ };
430
+ }
431
+
432
+ STATS['contribs'] = {
433
+ icon: icons.contribs,
434
+ label: i18n.t('statcard.contribs'),
435
+ value: contributedTo,
436
+ id: 'contribs',
437
+ };
438
+
439
+ if (show.includes('all_time_contribs')) {
440
+ STATS['all_time_contribs'] = {
441
+ icon: icons.contribs,
442
+ label: i18n.t('statcard.all-time-contribs'),
443
+ value: allTimeContributedTo,
444
+ id: 'all_time_contribs',
445
+ };
446
+ }
447
+
448
+ const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
449
+
450
+ // check if all used labels are short
451
+ const longLabels = Object.entries(STATS)
452
+ .filter(([key]) => !hide.includes(key))
453
+ .some(([, stat]) => stat.label.length > 18);
454
+
455
+ // filter out hidden stats defined by user & create the text nodes
456
+ const statItems = Object.entries(STATS)
457
+ .filter(([key]) => !hide.includes(key))
458
+ // pass index so that we can calculate the line spacing
459
+ .map(([, stat], index) =>
460
+ createTextNode({
461
+ icon: stat.icon,
462
+ label: stat.label,
463
+ value: stat.value,
464
+ id: stat.id,
465
+ unitSymbol: stat.unitSymbol,
466
+ index,
467
+ showIcons: show_icons,
468
+ shiftValuePos: 29.01 + (longLabels ? 50 : 0) + (isLongLocale ? 50 : 0),
469
+ bold: text_bold,
470
+ numberFormat: number_format,
471
+ numberPrecision: number_precision,
472
+ link: stat.link,
473
+ }),
474
+ );
475
+
476
+ if (statItems.length === 0 && hide_rank) {
477
+ throw new CardError('Could not render stats card.', {
478
+ code: 'invalid_param',
479
+ secondaryMessage: 'Either stats or rank are required.',
480
+ });
481
+ }
482
+
483
+ // Calculate the card height depending on how many items there are
484
+ // but if rank circle is visible clamp the minimum height to `150`
485
+ const height = Math.max(
486
+ 45 + (statItems.length + 1) * lheight,
487
+ hide_rank ? 0 : statItems.length ? 150 : 180,
488
+ );
489
+
490
+ // the lower the user's percentile the better
491
+ const progress = 100 - rank.percentile;
492
+
493
+ const calculateTextWidth = () => {
494
+ return measureText(
495
+ custom_title
496
+ ? custom_title
497
+ : statItems.length
498
+ ? i18n.t('statcard.title')
499
+ : i18n.t('statcard.ranktitle'),
500
+ );
501
+ };
502
+
503
+ /*
504
+ When hide_rank=true, the minimum card width is 270 px + the title length and padding.
505
+ When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
506
+ Numbers are picked by looking at existing dimensions on production.
507
+ */
508
+ const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
509
+ const minCardWidth =
510
+ (hide_rank
511
+ ? clampValue(50 /* padding */ + calculateTextWidth() * 2, CARD_MIN_WIDTH, Infinity)
512
+ : statItems.length
513
+ ? RANK_CARD_MIN_WIDTH
514
+ : RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
515
+ const defaultCardWidth =
516
+ (hide_rank
517
+ ? CARD_DEFAULT_WIDTH
518
+ : statItems.length
519
+ ? RANK_CARD_DEFAULT_WIDTH
520
+ : RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
521
+ const width = card_width
522
+ ? isNaN(card_width)
523
+ ? Math.max(defaultCardWidth, minCardWidth)
524
+ : card_width
525
+ : Math.max(defaultCardWidth, minCardWidth);
526
+
527
+ const card = new Card({
528
+ customTitle: custom_title,
529
+ defaultTitle: statItems.length ? i18n.t('statcard.title') : i18n.t('statcard.ranktitle'),
530
+ width,
531
+ height,
532
+ border_radius,
533
+ colors: { light: lightColors, dark: darkColors },
534
+ });
535
+
536
+ card.setHideBorder(hide_border);
537
+ card.setHideTitle(hide_title);
538
+ const cardStyles = ({ ringColor, textColor, iconColor }: CardColors): string =>
539
+ getStyles({ ringColor, textColor, iconColor, show_icons, progress });
540
+
541
+ card.setCSS({ light: cardStyles, dark: cardStyles });
542
+
543
+ if (disable_animations) {
544
+ card.disableAnimations();
545
+ }
546
+
547
+ /**
548
+ * Calculates the right rank circle translation values such that the rank circle
549
+ * keeps respecting the following padding:
550
+ *
551
+ * width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
552
+ * width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
553
+ * equally from a certain minimum at RANK_CARD_MIN_WIDTH.
554
+ *
555
+ * @returns Rank circle translation value.
556
+ */
557
+ const calculateRankXTranslation = (): number => {
558
+ if (statItems.length) {
559
+ const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
560
+ if (width > RANK_CARD_DEFAULT_WIDTH) {
561
+ const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
562
+ return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
563
+ } else {
564
+ return minXTranslation + (width - minCardWidth) / 2;
565
+ }
566
+ } else {
567
+ return width / 2 + 20 - 10;
568
+ }
569
+ };
570
+
571
+ // Conditionally rendered elements
572
+ const rankCircle = hide_rank
573
+ ? ''
574
+ : `<g data-testid="rank-circle"
575
+ transform="translate(${calculateRankXTranslation()}, ${height / 2 - 50})">
576
+ <circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
577
+ <circle class="rank-circle" cx="-10" cy="8" r="40" />
578
+ <g class="rank-text">
579
+ ${rankIcon(rank_icon, rank.level, rank.percentile)}
580
+ </g>
581
+ </g>`;
582
+
583
+ // Accessibility Labels
584
+ const labels = Object.entries(STATS)
585
+ .filter(([key]) => !hide.includes(key))
586
+ .map(([key, stat]) => {
587
+ if (key === 'commits') {
588
+ return `${i18n.t('statcard.commits')} ${getTotalCommitsYearLabel(
589
+ include_all_commits,
590
+ commits_year,
591
+ i18n,
592
+ )} : ${stat.value}`;
593
+ }
594
+ return `${stat.label}: ${stat.value}`;
595
+ })
596
+ .join(', ');
597
+
598
+ card.setAccessibilityLabel({
599
+ title: `${card.title}, Rank: ${rank.level}`,
600
+ desc: labels,
601
+ });
602
+
603
+ return card.render(`
604
+ ${rankCircle}
605
+ <svg x="0" y="0">
606
+ ${flexLayout({
607
+ items: statItems,
608
+ gap: lheight,
609
+ direction: 'column',
610
+ }).join('')}
611
+ </svg>
612
+ `);
613
+ };
614
+
615
+ export { RANK_ICONS, renderStatsCard };