@stats-organization/github-readme-stats-core 2.1.3 → 2.1.5

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 (123) hide show
  1. package/build/api/gist.d.ts.map +1 -1
  2. package/build/api/gist.js +34 -1
  3. package/build/api/index.d.ts.map +1 -1
  4. package/build/api/index.js +18 -1
  5. package/build/api/pin.d.ts.map +1 -1
  6. package/build/api/pin.js +17 -1
  7. package/build/api/top-langs.d.ts.map +1 -1
  8. package/build/api/top-langs.js +36 -5
  9. package/build/api/wakatime.d.ts.map +1 -1
  10. package/build/api/wakatime.js +34 -1
  11. package/build/calculateRank.d.ts +12 -11
  12. package/build/calculateRank.d.ts.map +1 -1
  13. package/build/calculateRank.js +18 -18
  14. package/build/cards/gist.d.ts.map +1 -1
  15. package/build/cards/gist.js +0 -1
  16. package/build/cards/repo.d.ts.map +1 -1
  17. package/build/cards/repo.js +29 -21
  18. package/build/cards/stats.d.ts +4 -2
  19. package/build/cards/stats.d.ts.map +1 -1
  20. package/build/cards/stats.js +21 -9
  21. package/build/cards/top-languages.d.ts.map +1 -1
  22. package/build/cards/top-languages.js +39 -12
  23. package/build/cards/wakatime.d.ts.map +1 -1
  24. package/build/cards/wakatime.js +29 -9
  25. package/build/common/Card.d.ts +54 -60
  26. package/build/common/Card.d.ts.map +1 -1
  27. package/build/common/Card.js +88 -61
  28. package/build/common/I18n.d.ts +13 -14
  29. package/build/common/I18n.d.ts.map +1 -1
  30. package/build/common/I18n.js +13 -12
  31. package/build/common/color.d.ts +56 -32
  32. package/build/common/color.d.ts.map +1 -1
  33. package/build/common/color.js +87 -50
  34. package/build/common/config.d.ts +18 -2
  35. package/build/common/config.d.ts.map +1 -1
  36. package/build/common/config.js +17 -26
  37. package/build/common/constants.d.ts +2 -2
  38. package/build/common/constants.d.ts.map +1 -1
  39. package/build/common/constants.js +5 -3
  40. package/build/common/error.d.ts +36 -29
  41. package/build/common/error.d.ts.map +1 -1
  42. package/build/common/error.js +14 -10
  43. package/build/common/fmt.d.ts +14 -13
  44. package/build/common/fmt.d.ts.map +1 -1
  45. package/build/common/fmt.js +19 -15
  46. package/build/common/html.d.ts +2 -4
  47. package/build/common/html.d.ts.map +1 -1
  48. package/build/common/html.js +2 -6
  49. package/build/common/http.d.ts +13 -4
  50. package/build/common/http.d.ts.map +1 -1
  51. package/build/common/http.js +3 -4
  52. package/build/common/icons.d.ts +22 -21
  53. package/build/common/icons.d.ts.map +1 -1
  54. package/build/common/icons.js +4 -5
  55. package/build/common/languageColors.json +9 -1
  56. package/build/common/ops.d.ts +30 -29
  57. package/build/common/ops.d.ts.map +1 -1
  58. package/build/common/ops.js +32 -38
  59. package/build/common/render.d.ts +115 -111
  60. package/build/common/render.d.ts.map +1 -1
  61. package/build/common/render.js +138 -88
  62. package/build/common/retryer.d.ts +24 -13
  63. package/build/common/retryer.d.ts.map +1 -1
  64. package/build/common/retryer.js +22 -30
  65. package/build/fetchers/gist.d.ts +6 -21
  66. package/build/fetchers/gist.d.ts.map +1 -1
  67. package/build/fetchers/gist.js +30 -36
  68. package/build/fetchers/repo.d.ts.map +1 -1
  69. package/build/fetchers/repo.js +4 -7
  70. package/build/fetchers/stats.d.ts.map +1 -1
  71. package/build/fetchers/stats.js +3 -7
  72. package/build/fetchers/top-languages.d.ts.map +1 -1
  73. package/build/fetchers/top-languages.js +0 -1
  74. package/build/fetchers/types.d.ts +126 -0
  75. package/build/fetchers/types.d.ts.map +1 -0
  76. package/build/fetchers/types.js +1 -0
  77. package/build/fetchers/wakatime.d.ts.map +1 -1
  78. package/build/fetchers/wakatime.js +0 -1
  79. package/build/translations.d.ts +627 -24
  80. package/build/translations.d.ts.map +1 -1
  81. package/build/translations.js +7 -8
  82. package/package.json +4 -4
  83. package/src/_emoji-name-map.d.ts +10 -0
  84. package/src/api/gist.js +36 -2
  85. package/src/api/index.js +19 -2
  86. package/src/api/pin.js +18 -2
  87. package/src/api/top-langs.js +38 -6
  88. package/src/api/wakatime.js +36 -2
  89. package/src/{calculateRank.js → calculateRank.ts} +29 -19
  90. package/src/cards/gist.js +0 -2
  91. package/src/cards/repo.js +30 -22
  92. package/src/cards/stats.js +22 -10
  93. package/src/cards/top-languages.js +49 -13
  94. package/src/cards/wakatime.js +33 -10
  95. package/src/common/Card.ts +338 -0
  96. package/src/common/I18n.ts +49 -0
  97. package/src/common/color.ts +212 -0
  98. package/src/common/config.ts +83 -0
  99. package/src/common/constants.ts +10 -0
  100. package/src/common/{error.js → error.ts} +20 -14
  101. package/src/common/{fmt.js → fmt.ts} +27 -19
  102. package/src/common/{html.js → html.ts} +3 -8
  103. package/src/common/http.ts +31 -0
  104. package/src/common/{icons.js → icons.ts} +9 -7
  105. package/src/common/languageColors.json +9 -1
  106. package/src/common/{ops.js → ops.ts} +48 -52
  107. package/src/common/{render.js → render.ts} +208 -98
  108. package/src/common/retryer.ts +125 -0
  109. package/src/fetchers/gist.ts +136 -0
  110. package/src/fetchers/repo.js +4 -8
  111. package/src/fetchers/stats.js +3 -8
  112. package/src/fetchers/top-languages.js +0 -2
  113. package/src/fetchers/{types.d.ts → types.ts} +1 -1
  114. package/src/fetchers/wakatime.js +0 -2
  115. package/src/{translations.js → translations.ts} +14 -10
  116. package/src/common/Card.js +0 -275
  117. package/src/common/I18n.js +0 -42
  118. package/src/common/color.js +0 -145
  119. package/src/common/config.js +0 -78
  120. package/src/common/constants.js +0 -9
  121. package/src/common/http.js +0 -21
  122. package/src/common/retryer.js +0 -104
  123. package/src/fetchers/gist.js +0 -114
@@ -1,16 +1,15 @@
1
- // @ts-check
2
1
  import { encodeHTML } from "./common/html.js";
3
2
  /**
4
3
  * Retrieves stat card labels in the available locales.
5
4
  *
6
- * @param {object} props Function arguments.
7
- * @param {string} props.name The name of the locale.
8
- * @param {string} props.apostrophe Whether to use apostrophe or not.
9
- * @returns {object} The locales object.
5
+ * @param props Function arguments.
6
+ * @param props.name The name of the locale.
7
+ * @param props.apostrophe Whether to use apostrophe or not.
8
+ * @returns The locales object.
10
9
  *
11
10
  * @see https://www.andiamo.co.uk/resources/iso-language-codes/ for language codes.
12
11
  */
13
- const statCardLocales = ({ name, apostrophe }) => {
12
+ const statCardLocales = ({ name, apostrophe, }) => {
14
13
  const encodedName = encodeHTML(name);
15
14
  return {
16
15
  "statcard.title": {
@@ -1131,8 +1130,8 @@ const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
1131
1130
  /**
1132
1131
  * Checks whether the locale is available or not.
1133
1132
  *
1134
- * @param {string} locale The locale to check.
1135
- * @returns {boolean} Boolean specifying whether the locale is available or not.
1133
+ * @param locale The locale to check.
1134
+ * @returns Boolean specifying whether the locale is available or not.
1136
1135
  */
1137
1136
  const isLocaleAvailable = (locale) => {
1138
1137
  return availableLocales.includes(locale.toLowerCase());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stats-organization/github-readme-stats-core",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "type": "module",
5
5
  "homepage": "https://github-stats-extended.vercel.app/frontend",
6
6
  "bugs": {
@@ -71,12 +71,12 @@
71
71
  "@testing-library/jest-dom": "6.9.1",
72
72
  "@uppercod/css-to-object": "1.1.1",
73
73
  "axios-mock-adapter": "2.1.0",
74
- "js-yaml": "4.2.0",
74
+ "js-yaml": "5.2.1",
75
75
  "jsdom": "29.1.1",
76
- "vitest": "4.1.8"
76
+ "vitest": "4.1.10"
77
77
  },
78
78
  "dependencies": {
79
- "axios": "^1.17.0",
79
+ "axios": "^1.18.1",
80
80
  "emoji-name-map": "^2.0.3",
81
81
  "github-username-regex": "^1.0.0"
82
82
  },
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `emoji-name-map` ships no type definitions and exposes no `exports` map, so it
3
+ * cannot be resolved under `nodenext`. Declare the minimal surface used here.
4
+ */
5
+ declare module "emoji-name-map" {
6
+ const emojiNameMap: {
7
+ get(name: string): string | undefined;
8
+ };
9
+ export default emojiNameMap;
10
+ }
package/src/api/gist.js CHANGED
@@ -1,6 +1,5 @@
1
- // @ts-check
2
-
3
1
  import { renderGistCard } from "../cards/gist.js";
2
+ import { findInvalidColor } from "../common/color.js";
4
3
  import {
5
4
  MissingParamError,
6
5
  retrieveSecondaryMessage,
@@ -28,6 +27,23 @@ export default async (
28
27
  },
29
28
  pat = null,
30
29
  ) => {
30
+ const invalidColorInput = findInvalidColor({
31
+ title_color,
32
+ icon_color,
33
+ text_color,
34
+ bg_color,
35
+ border_color,
36
+ });
37
+ if (invalidColorInput) {
38
+ return {
39
+ status: "error - permanent",
40
+ content: renderError({
41
+ message: "Something went wrong",
42
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
43
+ }),
44
+ };
45
+ }
46
+
31
47
  if (locale && !isLocaleAvailable(locale)) {
32
48
  return {
33
49
  status: "error - permanent",
@@ -45,6 +61,24 @@ export default async (
45
61
  };
46
62
  }
47
63
 
64
+ const safePattern = /^[-\w/.,]+$/;
65
+ if (id && !safePattern.test(id)) {
66
+ return {
67
+ status: "error - permanent",
68
+ content: renderError({
69
+ message: "Something went wrong",
70
+ secondaryMessage: "Gist ID contains unsafe characters",
71
+ renderOptions: {
72
+ title_color,
73
+ text_color,
74
+ bg_color,
75
+ border_color,
76
+ theme,
77
+ },
78
+ }),
79
+ };
80
+ }
81
+
48
82
  try {
49
83
  const gistData = await fetchGist(id, pat);
50
84
 
package/src/api/index.js CHANGED
@@ -1,6 +1,5 @@
1
- // @ts-check
2
-
3
1
  import { renderStatsCard } from "../cards/stats.js";
2
+ import { findInvalidColor } from "../common/color.js";
4
3
  import {
5
4
  MissingParamError,
6
5
  retrieveSecondaryMessage,
@@ -46,6 +45,24 @@ export default async (
46
45
  },
47
46
  pat = null,
48
47
  ) => {
48
+ const invalidColorInput = findInvalidColor({
49
+ title_color,
50
+ ring_color,
51
+ icon_color,
52
+ text_color,
53
+ bg_color,
54
+ border_color,
55
+ });
56
+ if (invalidColorInput) {
57
+ return {
58
+ status: "error - permanent",
59
+ content: renderError({
60
+ message: "Something went wrong",
61
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
62
+ }),
63
+ };
64
+ }
65
+
49
66
  if (locale && !isLocaleAvailable(locale)) {
50
67
  return {
51
68
  status: "error - permanent",
package/src/api/pin.js CHANGED
@@ -1,6 +1,5 @@
1
- // @ts-check
2
-
3
1
  import { renderRepoCard } from "../cards/repo.js";
2
+ import { findInvalidColor } from "../common/color.js";
4
3
  import {
5
4
  MissingParamError,
6
5
  retrieveSecondaryMessage,
@@ -36,6 +35,23 @@ export default async (
36
35
  },
37
36
  pat = null,
38
37
  ) => {
38
+ const invalidColorInput = findInvalidColor({
39
+ title_color,
40
+ icon_color,
41
+ text_color,
42
+ bg_color,
43
+ border_color,
44
+ });
45
+ if (invalidColorInput) {
46
+ return {
47
+ status: "error - permanent",
48
+ content: renderError({
49
+ message: "Something went wrong",
50
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
51
+ }),
52
+ };
53
+ }
54
+
39
55
  if (locale && !isLocaleAvailable(locale)) {
40
56
  return {
41
57
  status: "error - permanent",
@@ -1,6 +1,5 @@
1
- // @ts-check
2
-
3
1
  import { renderTopLanguages } from "../cards/top-languages.js";
2
+ import { findInvalidColor } from "../common/color.js";
4
3
  import {
5
4
  MissingParamError,
6
5
  retrieveSecondaryMessage,
@@ -40,6 +39,23 @@ export default async (
40
39
  },
41
40
  pat = null,
42
41
  ) => {
42
+ const invalidColorInput = findInvalidColor({
43
+ title_color,
44
+ text_color,
45
+ bg_color,
46
+ prog_bar_bg_color,
47
+ border_color,
48
+ });
49
+ if (invalidColorInput) {
50
+ return {
51
+ status: "error - permanent",
52
+ content: renderError({
53
+ message: "Something went wrong",
54
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
55
+ }),
56
+ };
57
+ }
58
+
43
59
  if (locale && !isLocaleAvailable(locale)) {
44
60
  return {
45
61
  status: "error - permanent",
@@ -57,10 +73,27 @@ export default async (
57
73
  };
58
74
  }
59
75
 
76
+ const safePattern = /^[-\w/.,]+$/;
77
+ if (username && !safePattern.test(username)) {
78
+ return {
79
+ status: "error - permanent",
80
+ content: renderError({
81
+ message: "Something went wrong",
82
+ secondaryMessage: "Username contains unsafe characters",
83
+ renderOptions: {
84
+ title_color,
85
+ text_color,
86
+ bg_color,
87
+ border_color,
88
+ theme,
89
+ },
90
+ }),
91
+ };
92
+ }
93
+
60
94
  if (
61
95
  layout !== undefined &&
62
- (typeof layout !== "string" ||
63
- !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout))
96
+ !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)
64
97
  ) {
65
98
  return {
66
99
  status: "error - permanent",
@@ -80,8 +113,7 @@ export default async (
80
113
 
81
114
  if (
82
115
  stats_format !== undefined &&
83
- (typeof stats_format !== "string" ||
84
- !["bytes", "percentages"].includes(stats_format))
116
+ !["bytes", "percentages"].includes(stats_format)
85
117
  ) {
86
118
  return {
87
119
  status: "error - permanent",
@@ -1,6 +1,5 @@
1
- // @ts-check
2
-
3
1
  import { renderWakatimeCard } from "../cards/wakatime.js";
2
+ import { findInvalidColor } from "../common/color.js";
4
3
  import {
5
4
  MissingParamError,
6
5
  retrieveSecondaryMessage,
@@ -34,6 +33,23 @@ export default async ({
34
33
  display_format,
35
34
  disable_animations,
36
35
  }) => {
36
+ const invalidColorInput = findInvalidColor({
37
+ title_color,
38
+ icon_color,
39
+ text_color,
40
+ bg_color,
41
+ border_color,
42
+ });
43
+ if (invalidColorInput) {
44
+ return {
45
+ status: "error - permanent",
46
+ content: renderError({
47
+ message: "Something went wrong",
48
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
49
+ }),
50
+ };
51
+ }
52
+
37
53
  if (locale && !isLocaleAvailable(locale)) {
38
54
  return {
39
55
  status: "error - permanent",
@@ -51,6 +67,24 @@ export default async ({
51
67
  };
52
68
  }
53
69
 
70
+ const safePattern = /^[-\w/.,]+$/;
71
+ if (username && !safePattern.test(username)) {
72
+ return {
73
+ status: "error - permanent",
74
+ content: renderError({
75
+ message: "Something went wrong",
76
+ secondaryMessage: "Username contains unsafe characters",
77
+ renderOptions: {
78
+ title_color,
79
+ text_color,
80
+ bg_color,
81
+ border_color,
82
+ theme,
83
+ },
84
+ }),
85
+ };
86
+ }
87
+
54
88
  try {
55
89
  const stats = await fetchWakatimeStats({ username, api_domain });
56
90
 
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Calculates the exponential cdf.
3
3
  *
4
- * @param {number} x The value.
5
- * @returns {number} The exponential cdf.
4
+ * @param x The value.
5
+ * @returns The exponential cdf.
6
6
  */
7
- function exponential_cdf(x) {
7
+ function exponential_cdf(x: number): number {
8
8
  return 1 - 2 ** -x;
9
9
  }
10
10
 
11
11
  /**
12
12
  * Calculates the log normal cdf.
13
13
  *
14
- * @param {number} x The value.
15
- * @returns {number} The log normal cdf.
14
+ * @param x The value.
15
+ * @returns The log normal cdf.
16
16
  */
17
- function log_normal_cdf(x) {
17
+ function log_normal_cdf(x: number): number {
18
18
  // approximation
19
19
  return x / (1 + x);
20
20
  }
@@ -22,16 +22,16 @@ function log_normal_cdf(x) {
22
22
  /**
23
23
  * Calculates the users rank.
24
24
  *
25
- * @param {object} params Parameters on which the user's rank depends.
26
- * @param {boolean} params.all_commits Whether `include_all_commits` was used.
27
- * @param {number} params.commits Number of commits.
28
- * @param {number} params.prs The number of pull requests.
29
- * @param {number} params.issues The number of issues.
30
- * @param {number} params.reviews The number of reviews.
31
- * @param {number} params.repos Total number of repos.
32
- * @param {number} params.stars The number of stars.
33
- * @param {number} params.followers The number of followers.
34
- * @returns {{ level: string, percentile: number }} The users rank.
25
+ * @param params Parameters on which the user's rank depends.
26
+ * @param params.all_commits Whether `include_all_commits` was used.
27
+ * @param params.commits Number of commits.
28
+ * @param params.prs The number of pull requests.
29
+ * @param params.issues The number of issues.
30
+ * @param params.reviews The number of reviews.
31
+ * @param params.repos Total number of repos (accepted for compatibility, unused in the calculation).
32
+ * @param params.stars The number of stars.
33
+ * @param params.followers The number of followers.
34
+ * @returns The users rank.
35
35
  */
36
36
  function calculateRank({
37
37
  all_commits,
@@ -39,11 +39,18 @@ function calculateRank({
39
39
  prs,
40
40
  issues,
41
41
  reviews,
42
- // eslint-disable-next-line no-unused-vars
43
- repos, // unused
44
42
  stars,
45
43
  followers,
46
- }) {
44
+ }: {
45
+ all_commits: boolean;
46
+ commits: number;
47
+ prs: number;
48
+ issues: number;
49
+ reviews: number;
50
+ repos: number;
51
+ stars: number;
52
+ followers: number;
53
+ }): { level: string; percentile: number } {
47
54
  const COMMITS_MEDIAN = all_commits ? 1000 : 250,
48
55
  COMMITS_WEIGHT = 2;
49
56
  const PRS_MEDIAN = 50,
@@ -79,6 +86,9 @@ function calculateRank({
79
86
  TOTAL_WEIGHT;
80
87
 
81
88
  const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];
89
+ if (level === undefined) {
90
+ throw new Error("Unable to determine rank level");
91
+ }
82
92
 
83
93
  return { level, percentile: rank * 100 };
84
94
  }
package/src/cards/gist.js CHANGED
@@ -1,5 +1,3 @@
1
- // @ts-check
2
-
3
1
  import { default as Card } from "../common/Card.js";
4
2
  import { getCardColors } from "../common/color.js";
5
3
  import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
package/src/cards/repo.js CHANGED
@@ -1,8 +1,6 @@
1
- // @ts-check
2
-
3
1
  import { Card } from "../common/Card.js";
4
2
  import { I18n } from "../common/I18n.js";
5
- import { getCardColors } from "../common/color.js";
3
+ import { getCardColors, isPrefixedHexColor } from "../common/color.js";
6
4
  import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
7
5
  import { encodeHTML } from "../common/html.js";
8
6
  import { icons } from "../common/icons.js";
@@ -34,20 +32,29 @@ const DESCRIPTION_MAX_LINES = 3;
34
32
  * @param {string} textColor The color of the text.
35
33
  * @returns {string} Wrapped repo description SVG object.
36
34
  */
37
- const getBadgeSVG = (label, textColor, xOffset = 0) => `
38
- <g data-testid="badge" class="badge" transform="translate(${320 + xOffset}, -18)">
39
- <rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
40
- <text
41
- x="23" y="-5"
42
- alignment-baseline="central"
43
- dominant-baseline="central"
44
- text-anchor="middle"
45
- fill="${textColor}"
46
- >
47
- ${label}
48
- </text>
49
- </g>
50
- `;
35
+ const getBadgeSVG = (label, textColor, xOffset = 0) => {
36
+ if (!isPrefixedHexColor(textColor)) {
37
+ throw new Error(`Invalid text color: "${textColor}"`);
38
+ }
39
+ if (!Number.isFinite(xOffset)) {
40
+ throw new Error(`Invalid xOffset: "${xOffset}"`);
41
+ }
42
+
43
+ return `
44
+ <g data-testid="badge" class="badge" transform="translate(${320 + xOffset}, -18)">
45
+ <rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
46
+ <text
47
+ x="23" y="-5"
48
+ alignment-baseline="central"
49
+ dominant-baseline="central"
50
+ text-anchor="middle"
51
+ fill="${textColor}"
52
+ >
53
+ ${encodeHTML(label)}
54
+ </text>
55
+ </g>
56
+ `;
57
+ };
51
58
 
52
59
  /**
53
60
  * @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
@@ -112,6 +119,7 @@ const renderRepoCard = (repo, options = {}) => {
112
119
  });
113
120
 
114
121
  let repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
122
+ const encodedUsername = encodeURIComponent(username);
115
123
  const STATS = {};
116
124
  if (show.includes("prs_authored")) {
117
125
  STATS.prs_authored = {
@@ -119,7 +127,7 @@ const renderRepoCard = (repo, options = {}) => {
119
127
  label: i18n.t("repocard.prs-authored"),
120
128
  value: totalPRsAuthored,
121
129
  id: "prs_authored",
122
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=pullrequests`,
130
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
123
131
  };
124
132
  }
125
133
  if (show.includes("prs_commented")) {
@@ -128,7 +136,7 @@ const renderRepoCard = (repo, options = {}) => {
128
136
  label: i18n.t("repocard.prs-commented"),
129
137
  value: totalPRsCommented,
130
138
  id: "prs_commented",
131
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=pullrequests`,
139
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
132
140
  };
133
141
  }
134
142
  if (show.includes("prs_reviewed")) {
@@ -137,7 +145,7 @@ const renderRepoCard = (repo, options = {}) => {
137
145
  label: i18n.t("repocard.prs-reviewed"),
138
146
  value: totalPRsReviewed,
139
147
  id: "prs_reviewed",
140
- link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${username}+-author%3A${username}&amp;type=pullrequests`,
148
+ link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
141
149
  };
142
150
  }
143
151
  if (show.includes("issues_authored")) {
@@ -146,7 +154,7 @@ const renderRepoCard = (repo, options = {}) => {
146
154
  label: i18n.t("repocard.issues-authored"),
147
155
  value: totalIssuesAuthored,
148
156
  id: "issues_authored",
149
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=issues`,
157
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
150
158
  };
151
159
  }
152
160
  if (show.includes("issues_commented")) {
@@ -155,7 +163,7 @@ const renderRepoCard = (repo, options = {}) => {
155
163
  label: i18n.t("repocard.issues-commented"),
156
164
  value: totalIssuesCommented,
157
165
  id: "issues_commented",
158
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=issues`,
166
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=issues`,
159
167
  };
160
168
  }
161
169
 
@@ -1,10 +1,9 @@
1
- // @ts-check
2
-
3
1
  import { Card } from "../common/Card.js";
4
2
  import { I18n } from "../common/I18n.js";
5
3
  import { getCardColors } from "../common/color.js";
6
4
  import { CustomError } from "../common/error.js";
7
5
  import { kFormatter } from "../common/fmt.js";
6
+ import { encodeHTML } from "../common/html.js";
8
7
  import { icons, rankIcon } from "../common/icons.js";
9
8
  import { buildSearchFilter, clampValue } from "../common/ops.js";
10
9
  import { flexLayout, measureText } from "../common/render.js";
@@ -54,8 +53,10 @@ const LONG_LOCALES = [
54
53
  /**
55
54
  * Create a stats card text item.
56
55
  *
56
+ * The caller must ensure that the passed `icon` and `link` are properly sanitized!
57
+ *
57
58
  * @param {object} params Object that contains the createTextNode parameters.
58
- * @param {string} params.icon The icon to display.
59
+ * @param {string} params.icon The sanitized icon to display.
59
60
  * @param {string} params.label The label to display.
60
61
  * @param {number} params.value The value to display.
61
62
  * @param {string} params.id The id of the stat.
@@ -66,7 +67,7 @@ const LONG_LOCALES = [
66
67
  * @param {boolean} params.bold Whether to bold the label.
67
68
  * @param {string} params.numberFormat The format of numbers on card.
68
69
  * @param {number=} params.numberPrecision The precision of numbers on card.
69
- * @param {string} params.link Url to link to.
70
+ * @param {string} params.link Sanitized url to link to.
70
71
  * @param {number} params.labelXOffset horizontal offset for label.
71
72
  * @returns {string} The stats card text item SVG object.
72
73
  */
@@ -85,6 +86,16 @@ const createTextNode = ({
85
86
  link,
86
87
  labelXOffset = 25,
87
88
  }) => {
89
+ if (!Number.isFinite(labelXOffset)) {
90
+ throw new Error(`Invalid labelXOffset: "${labelXOffset}"`);
91
+ }
92
+ if (!Number.isFinite(shiftValuePos)) {
93
+ throw new Error(`Invalid shiftValuePos: "${shiftValuePos}"`);
94
+ }
95
+ if (!Number.isFinite(index)) {
96
+ throw new Error(`Invalid index: "${index}"`);
97
+ }
98
+
88
99
  const precision =
89
100
  typeof numberPrecision === "number" && !isNaN(numberPrecision)
90
101
  ? clampValue(numberPrecision, 0, 2)
@@ -111,7 +122,7 @@ const createTextNode = ({
111
122
  ${iconSvg}
112
123
  <text class="stat ${
113
124
  bold ? " bold" : "not_bold"
114
- }" ${labelOffset} y="12.5">${label}:</text>
125
+ }" ${labelOffset} y="12.5">${encodeHTML(label)}:</text>
115
126
  <text
116
127
  class="stat ${bold ? " bold" : "not_bold"}"
117
128
  x="${(showIcons ? 140 : 120) + (bold ? 5 : 0) + shiftValuePos}"
@@ -424,13 +435,14 @@ const renderStatsCard = (
424
435
  }
425
436
 
426
437
  let repoFilter = encodeURIComponent(buildSearchFilter(repo, owner));
438
+ const encodedUsername = encodeURIComponent(username);
427
439
  if (show.includes("prs_authored")) {
428
440
  STATS.prs_authored = {
429
441
  icon: icons.prs,
430
442
  label: i18n.t("statcard.prs-authored"),
431
443
  value: totalPRsAuthored,
432
444
  id: "prs_authored",
433
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=pullrequests`,
445
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
434
446
  };
435
447
  }
436
448
  if (show.includes("prs_commented")) {
@@ -439,7 +451,7 @@ const renderStatsCard = (
439
451
  label: i18n.t("statcard.prs-commented"),
440
452
  value: totalPRsCommented,
441
453
  id: "prs_commented",
442
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=pullrequests`,
454
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
443
455
  };
444
456
  }
445
457
  if (show.includes("prs_reviewed")) {
@@ -448,7 +460,7 @@ const renderStatsCard = (
448
460
  label: i18n.t("statcard.prs-reviewed"),
449
461
  value: totalPRsReviewed,
450
462
  id: "prs_reviewed",
451
- link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${username}+-author%3A${username}&amp;type=pullrequests`,
463
+ link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
452
464
  };
453
465
  }
454
466
  if (show.includes("issues_authored")) {
@@ -457,7 +469,7 @@ const renderStatsCard = (
457
469
  label: i18n.t("statcard.issues-authored"),
458
470
  value: totalIssuesAuthored,
459
471
  id: "issues_authored",
460
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=issues`,
472
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
461
473
  };
462
474
  }
463
475
  if (show.includes("issues_commented")) {
@@ -466,7 +478,7 @@ const renderStatsCard = (
466
478
  label: i18n.t("statcard.issues-commented"),
467
479
  value: totalIssuesCommented,
468
480
  id: "issues_commented",
469
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=issues`,
481
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=issues`,
470
482
  };
471
483
  }
472
484