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