@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,153 @@
1
+ // @ts-check
2
+
3
+ import { clampValue } from "./ops.js";
4
+
5
+ const MIN = 60;
6
+ const HOUR = 60 * MIN;
7
+ const DAY = 24 * HOUR;
8
+
9
+ /**
10
+ * Common durations in seconds.
11
+ */
12
+ const DURATIONS = {
13
+ ONE_MINUTE: MIN,
14
+ FIVE_MINUTES: 5 * MIN,
15
+ TEN_MINUTES: 10 * MIN,
16
+ FIFTEEN_MINUTES: 15 * MIN,
17
+ THIRTY_MINUTES: 30 * MIN,
18
+
19
+ TWO_HOURS: 2 * HOUR,
20
+ FOUR_HOURS: 4 * HOUR,
21
+ SIX_HOURS: 6 * HOUR,
22
+ EIGHT_HOURS: 8 * HOUR,
23
+ TWELVE_HOURS: 12 * HOUR,
24
+
25
+ ONE_DAY: DAY,
26
+ TWO_DAY: 2 * DAY,
27
+ SIX_DAY: 6 * DAY,
28
+ TEN_DAY: 10 * DAY,
29
+ };
30
+
31
+ /**
32
+ * Common cache TTL values in seconds.
33
+ */
34
+ const CACHE_TTL = {
35
+ STATS_CARD: {
36
+ DEFAULT: DURATIONS.ONE_DAY,
37
+ MIN: DURATIONS.TWELVE_HOURS,
38
+ MAX: DURATIONS.TWO_DAY,
39
+ },
40
+ TOP_LANGS_CARD: {
41
+ DEFAULT: DURATIONS.SIX_DAY,
42
+ MIN: DURATIONS.TWO_DAY,
43
+ MAX: DURATIONS.TEN_DAY,
44
+ },
45
+ PIN_CARD: {
46
+ DEFAULT: DURATIONS.TEN_DAY,
47
+ MIN: DURATIONS.ONE_DAY,
48
+ MAX: DURATIONS.TEN_DAY,
49
+ },
50
+ GIST_CARD: {
51
+ DEFAULT: DURATIONS.TWO_DAY,
52
+ MIN: DURATIONS.ONE_DAY,
53
+ MAX: DURATIONS.TEN_DAY,
54
+ },
55
+ WAKATIME_CARD: {
56
+ DEFAULT: DURATIONS.ONE_DAY,
57
+ MIN: DURATIONS.TWELVE_HOURS,
58
+ MAX: DURATIONS.TWO_DAY,
59
+ },
60
+ ERROR: DURATIONS.TEN_MINUTES,
61
+ };
62
+
63
+ /**
64
+ * Resolves the cache seconds based on the requested, default, min, and max values.
65
+ *
66
+ * @param {Object} args The parameters object.
67
+ * @param {number} args.requested The requested cache seconds.
68
+ * @param {number} args.def The default cache seconds.
69
+ * @param {number} args.min The minimum cache seconds.
70
+ * @param {number} args.max The maximum cache seconds.
71
+ * @returns {number} The resolved cache seconds.
72
+ */
73
+ const resolveCacheSeconds = ({ requested, def, min, max }) => {
74
+ let cacheSeconds = clampValue(isNaN(requested) ? def : requested, min, max);
75
+
76
+ if (process.env.CACHE_SECONDS) {
77
+ const envCacheSeconds = parseInt(process.env.CACHE_SECONDS, 10);
78
+ if (!isNaN(envCacheSeconds)) {
79
+ cacheSeconds = envCacheSeconds;
80
+ }
81
+ }
82
+
83
+ return cacheSeconds;
84
+ };
85
+
86
+ /**
87
+ * Disables caching by setting appropriate headers on the response object.
88
+ *
89
+ * @param {any} res The response object.
90
+ */
91
+ const disableCaching = (res) => {
92
+ // Disable caching for browsers, shared caches/CDNs, and GitHub Camo.
93
+ res.setHeader(
94
+ "Cache-Control",
95
+ "no-cache, no-store, must-revalidate, max-age=0, s-maxage=0",
96
+ );
97
+ res.setHeader("Pragma", "no-cache");
98
+ res.setHeader("Expires", "0");
99
+ };
100
+
101
+ /**
102
+ * Sets the Cache-Control headers on the response object.
103
+ *
104
+ * @param {any} res The response object.
105
+ * @param {number} cacheSeconds The cache seconds to set in the headers.
106
+ */
107
+ const setCacheHeaders = (res, cacheSeconds) => {
108
+ if (cacheSeconds < 1 || process.env.NODE_ENV === "development") {
109
+ disableCaching(res);
110
+ return;
111
+ }
112
+
113
+ res.setHeader(
114
+ "Cache-Control",
115
+ `max-age=${cacheSeconds}, ` +
116
+ `s-maxage=${cacheSeconds}, ` +
117
+ `stale-while-revalidate=${DURATIONS.ONE_DAY}`,
118
+ );
119
+ };
120
+
121
+ /**
122
+ * Sets the Cache-Control headers for error responses on the response object.
123
+ *
124
+ * @param {any} res The response object.
125
+ */
126
+ const setErrorCacheHeaders = (res) => {
127
+ const envCacheSeconds = process.env.CACHE_SECONDS
128
+ ? parseInt(process.env.CACHE_SECONDS, 10)
129
+ : NaN;
130
+ if (
131
+ (!isNaN(envCacheSeconds) && envCacheSeconds < 1) ||
132
+ process.env.NODE_ENV === "development"
133
+ ) {
134
+ disableCaching(res);
135
+ return;
136
+ }
137
+
138
+ // Use lower cache period for errors.
139
+ res.setHeader(
140
+ "Cache-Control",
141
+ `max-age=${CACHE_TTL.ERROR}, ` +
142
+ `s-maxage=${CACHE_TTL.ERROR}, ` +
143
+ `stale-while-revalidate=${DURATIONS.ONE_DAY}`,
144
+ );
145
+ };
146
+
147
+ export {
148
+ resolveCacheSeconds,
149
+ setCacheHeaders,
150
+ setErrorCacheHeaders,
151
+ DURATIONS,
152
+ CACHE_TTL,
153
+ };
@@ -0,0 +1,194 @@
1
+ // @ts-check
2
+
3
+ import { themes } from "../../themes/index.js";
4
+
5
+ /**
6
+ * Checks if a string is a valid hex color.
7
+ *
8
+ * @param {string} hexColor String to check.
9
+ * @returns {boolean} True if the given string is a valid hex color.
10
+ */
11
+ const isValidHexColor = (hexColor) => {
12
+ return new RegExp(
13
+ /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
14
+ ).test(hexColor);
15
+ };
16
+
17
+ /**
18
+ * Check if the given string is a valid gradient.
19
+ *
20
+ * @param {string[]} colors Array of colors.
21
+ * @returns {boolean} True if the given string is a valid gradient.
22
+ */
23
+ const isValidGradient = (colors) => {
24
+ return (
25
+ colors.length > 2 &&
26
+ colors.slice(1).every((color) => isValidHexColor(color))
27
+ );
28
+ };
29
+
30
+ /**
31
+ * Retrieves a gradient if color has more than one valid hex codes else a single color.
32
+ *
33
+ * @param {string} color The color to parse.
34
+ * @param {string | string[]} fallbackColor The fallback color.
35
+ * @returns {string | string[]} The gradient or color.
36
+ */
37
+ const fallbackColor = (color, fallbackColor) => {
38
+ let gradient = null;
39
+
40
+ let colors = color ? color.split(",") : [];
41
+ if (colors.length > 1 && isValidGradient(colors)) {
42
+ gradient = colors;
43
+ }
44
+
45
+ return (
46
+ (gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
47
+ fallbackColor
48
+ );
49
+ };
50
+
51
+ /**
52
+ * Object containing card colors.
53
+ * @typedef {{
54
+ * titleColor: string;
55
+ * iconColor: string;
56
+ * textColor: string;
57
+ * bgColor: string | string[];
58
+ * borderColor: string;
59
+ * ringColor: string;
60
+ * }} CardColors
61
+ */
62
+
63
+ /**
64
+ * Returns theme based colors with proper overrides and defaults.
65
+ *
66
+ * @param {Object} args Function arguments.
67
+ * @param {string=} args.title_color Card title color.
68
+ * @param {string=} args.text_color Card text color.
69
+ * @param {string=} args.icon_color Card icon color.
70
+ * @param {string=} args.bg_color Card background color.
71
+ * @param {string=} args.border_color Card border color.
72
+ * @param {string=} args.ring_color Card ring color.
73
+ * @param {string=} args.theme Card theme.
74
+ * @returns {CardColors} Card colors.
75
+ */
76
+ const getCardColors = ({
77
+ title_color,
78
+ text_color,
79
+ icon_color,
80
+ bg_color,
81
+ border_color,
82
+ ring_color,
83
+ theme,
84
+ }) => {
85
+ const defaultTheme = themes["default"];
86
+ const isThemeProvided = theme !== null && theme !== undefined;
87
+
88
+ // @ts-ignore
89
+ const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
90
+
91
+ const defaultBorderColor =
92
+ "border_color" in selectedTheme
93
+ ? selectedTheme.border_color
94
+ : // @ts-ignore
95
+ defaultTheme.border_color;
96
+
97
+ // get the color provided by the user else the theme color
98
+ // finally if both colors are invalid fallback to default theme
99
+ const titleColor = fallbackColor(
100
+ title_color || selectedTheme.title_color,
101
+ "#" + defaultTheme.title_color,
102
+ );
103
+
104
+ // get the color provided by the user else the theme color
105
+ // finally if both colors are invalid we use the titleColor
106
+ const ringColor = fallbackColor(
107
+ // @ts-ignore
108
+ ring_color || selectedTheme.ring_color,
109
+ titleColor,
110
+ );
111
+ const iconColor = fallbackColor(
112
+ icon_color || selectedTheme.icon_color,
113
+ "#" + defaultTheme.icon_color,
114
+ );
115
+ const textColor = fallbackColor(
116
+ text_color || selectedTheme.text_color,
117
+ "#" + defaultTheme.text_color,
118
+ );
119
+ const bgColor = fallbackColor(
120
+ bg_color || selectedTheme.bg_color,
121
+ "#" + defaultTheme.bg_color,
122
+ );
123
+
124
+ const borderColor = fallbackColor(
125
+ border_color || defaultBorderColor,
126
+ "#" + defaultBorderColor,
127
+ );
128
+
129
+ if (
130
+ typeof titleColor !== "string" ||
131
+ typeof textColor !== "string" ||
132
+ typeof ringColor !== "string" ||
133
+ typeof iconColor !== "string" ||
134
+ typeof borderColor !== "string"
135
+ ) {
136
+ throw new Error(
137
+ "Unexpected behavior, all colors except background should be string.",
138
+ );
139
+ }
140
+
141
+ return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
142
+ };
143
+
144
+ /**
145
+ * Validates and canonicalizes color parameters to prevent XSS.
146
+ * Returns a canonicalized color string (hex digits without #) for valid colors,
147
+ * or undefined for invalid colors, allowing renderError to use safe defaults.
148
+ * This ensures we never output user-controlled strings directly.
149
+ * Note: Returns without # prefix to match getCardColors expectations.
150
+ *
151
+ * @param {string|undefined} color The color value to validate and canonicalize.
152
+ * @returns {string|undefined} Canonicalized color (hex without #) or undefined.
153
+ */
154
+ const validateColor = (color) => {
155
+ if (!color || typeof color !== "string") {
156
+ return undefined;
157
+ }
158
+ // Remove leading # if present and trim whitespace
159
+ const hexColor = color.replace(/^#/, "").trim();
160
+ // Only allow 3, 4, 6, or 8 digit hex codes - strict validation
161
+ if (
162
+ /^(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(
163
+ hexColor,
164
+ )
165
+ ) {
166
+ // Return canonicalized format: validated hex lowercase (never the original user string)
167
+ // Note: No # prefix as getCardColors expects colors without #
168
+ return hexColor.toLowerCase();
169
+ }
170
+ return undefined;
171
+ };
172
+
173
+ /**
174
+ * Validates theme parameter to prevent XSS.
175
+ * Returns undefined for invalid themes, allowing renderError to use safe defaults.
176
+ *
177
+ * @param {string|undefined} theme The theme name to validate.
178
+ * @returns {string|undefined} Validated theme name or undefined.
179
+ */
180
+ const validateTheme = (theme) => {
181
+ if (!theme || typeof theme !== "string") {
182
+ return undefined;
183
+ }
184
+ // Check if theme exists in themes object (whitelist validation)
185
+ return themes[theme] ? theme : undefined;
186
+ };
187
+
188
+ export {
189
+ isValidHexColor,
190
+ isValidGradient,
191
+ getCardColors,
192
+ validateColor,
193
+ validateTheme,
194
+ };
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+
3
+ const whitelist = process.env.WHITELIST
4
+ ? process.env.WHITELIST.split(",")
5
+ : undefined;
6
+
7
+ const gistWhitelist = process.env.GIST_WHITELIST
8
+ ? process.env.GIST_WHITELIST.split(",")
9
+ : undefined;
10
+
11
+ const excludeRepositories = process.env.EXCLUDE_REPO
12
+ ? process.env.EXCLUDE_REPO.split(",")
13
+ : [];
14
+
15
+ export { whitelist, gistWhitelist, excludeRepositories };
@@ -0,0 +1,84 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @type {string} A general message to ask user to try again later.
5
+ */
6
+ const TRY_AGAIN_LATER = "Please try again later";
7
+
8
+ /**
9
+ * @type {Object<string, string>} A map of error types to secondary error messages.
10
+ */
11
+ const SECONDARY_ERROR_MESSAGES = {
12
+ MAX_RETRY:
13
+ "You can deploy own instance or wait until public will be no longer limited",
14
+ NO_TOKENS:
15
+ "Please add an env variable called PAT_1 with your GitHub API token in vercel",
16
+ USER_NOT_FOUND: "Make sure the provided username is not an organization",
17
+ GRAPHQL_ERROR: TRY_AGAIN_LATER,
18
+ GITHUB_REST_API_ERROR: TRY_AGAIN_LATER,
19
+ WAKATIME_USER_NOT_FOUND: "Make sure you have a public WakaTime profile",
20
+ };
21
+
22
+ /**
23
+ * Custom error class to handle custom GRS errors.
24
+ */
25
+ class CustomError extends Error {
26
+ /**
27
+ * Custom error constructor.
28
+ *
29
+ * @param {string} message Error message.
30
+ * @param {string} type Error type.
31
+ */
32
+ constructor(message, type) {
33
+ super(message);
34
+ this.type = type;
35
+ this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
36
+ }
37
+
38
+ static MAX_RETRY = "MAX_RETRY";
39
+ static NO_TOKENS = "NO_TOKENS";
40
+ static USER_NOT_FOUND = "USER_NOT_FOUND";
41
+ static GRAPHQL_ERROR = "GRAPHQL_ERROR";
42
+ static GITHUB_REST_API_ERROR = "GITHUB_REST_API_ERROR";
43
+ static WAKATIME_ERROR = "WAKATIME_ERROR";
44
+ }
45
+
46
+ /**
47
+ * Missing query parameter class.
48
+ */
49
+ class MissingParamError extends Error {
50
+ /**
51
+ * Missing query parameter error constructor.
52
+ *
53
+ * @param {string[]} missedParams An array of missing parameters names.
54
+ * @param {string=} secondaryMessage Optional secondary message to display.
55
+ */
56
+ constructor(missedParams, secondaryMessage) {
57
+ const msg = `Missing params ${missedParams
58
+ .map((p) => `"${p}"`)
59
+ .join(", ")} make sure you pass the parameters in URL`;
60
+ super(msg);
61
+ this.missedParams = missedParams;
62
+ this.secondaryMessage = secondaryMessage;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Retrieve secondary message from an error object.
68
+ *
69
+ * @param {Error} err The error object.
70
+ * @returns {string|undefined} The secondary message if available, otherwise undefined.
71
+ */
72
+ const retrieveSecondaryMessage = (err) => {
73
+ return "secondaryMessage" in err && typeof err.secondaryMessage === "string"
74
+ ? err.secondaryMessage
75
+ : undefined;
76
+ };
77
+
78
+ export {
79
+ CustomError,
80
+ MissingParamError,
81
+ SECONDARY_ERROR_MESSAGES,
82
+ TRY_AGAIN_LATER,
83
+ retrieveSecondaryMessage,
84
+ };
@@ -0,0 +1,90 @@
1
+ // @ts-check
2
+
3
+ import wrap from "word-wrap";
4
+ import { encodeHTML } from "./html.js";
5
+
6
+ /**
7
+ * Retrieves num with suffix k(thousands) precise to given decimal places.
8
+ *
9
+ * @param {number} num The number to format.
10
+ * @param {number=} precision The number of decimal places to include.
11
+ * @returns {string|number} The formatted number.
12
+ */
13
+ const kFormatter = (num, precision) => {
14
+ const abs = Math.abs(num);
15
+ const sign = Math.sign(num);
16
+
17
+ if (typeof precision === "number" && !isNaN(precision)) {
18
+ return (sign * (abs / 1000)).toFixed(precision) + "k";
19
+ }
20
+
21
+ if (abs < 1000) {
22
+ return sign * abs;
23
+ }
24
+
25
+ return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
26
+ };
27
+
28
+ /**
29
+ * Convert bytes to a human-readable string representation.
30
+ *
31
+ * @param {number} bytes The number of bytes to convert.
32
+ * @returns {string} The human-readable representation of bytes.
33
+ * @throws {Error} If bytes is negative or too large.
34
+ */
35
+ const formatBytes = (bytes) => {
36
+ if (bytes < 0) {
37
+ throw new Error("Bytes must be a non-negative number");
38
+ }
39
+
40
+ if (bytes === 0) {
41
+ return "0 B";
42
+ }
43
+
44
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
45
+ const base = 1024;
46
+ const i = Math.floor(Math.log(bytes) / Math.log(base));
47
+
48
+ if (i >= sizes.length) {
49
+ throw new Error("Bytes is too large to convert to a human-readable string");
50
+ }
51
+
52
+ return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
53
+ };
54
+
55
+ /**
56
+ * Split text over multiple lines based on the card width.
57
+ *
58
+ * @param {string} text Text to split.
59
+ * @param {number} width Line width in number of characters.
60
+ * @param {number} maxLines Maximum number of lines.
61
+ * @returns {string[]} Array of lines.
62
+ */
63
+ const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
64
+ const fullWidthComma = ",";
65
+ const encoded = encodeHTML(text);
66
+ const isChinese = encoded.includes(fullWidthComma);
67
+
68
+ let wrapped = [];
69
+
70
+ if (isChinese) {
71
+ wrapped = encoded.split(fullWidthComma); // Chinese full punctuation
72
+ } else {
73
+ wrapped = wrap(encoded, {
74
+ width,
75
+ }).split("\n"); // Split wrapped lines to get an array of lines
76
+ }
77
+
78
+ const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
79
+
80
+ // Add "..." to the last line if the text exceeds maxLines
81
+ if (wrapped.length > maxLines) {
82
+ lines[maxLines - 1] += "...";
83
+ }
84
+
85
+ // Remove empty lines if text fits in less than maxLines lines
86
+ const multiLineText = lines.filter(Boolean);
87
+ return multiLineText;
88
+ };
89
+
90
+ export { kFormatter, formatBytes, wrapTextMultiline };
@@ -0,0 +1,43 @@
1
+ // @ts-check
2
+
3
+ import escapeHtml from "escape-html";
4
+
5
+ /**
6
+ * Encode string as HTML to prevent XSS.
7
+ * Uses the well-known escape-html library which encodes &, <, >, ", '.
8
+ * This is recognized by security scanners like CodeQL.
9
+ *
10
+ * @param {string} str String to encode.
11
+ * @returns {string} Encoded string.
12
+ */
13
+ const encodeHTML = (str) => {
14
+ // escape-html handles XSS-critical characters: & < > " '
15
+ // Also remove backspace character which could cause display issues
16
+ return escapeHtml(str).replace(/\u0008/gim, "");
17
+ };
18
+
19
+ /**
20
+ * Escape CSS/attribute value to prevent XSS in SVG attributes.
21
+ * This function ensures that color values and other CSS values
22
+ * are safe to use in SVG attribute contexts.
23
+ *
24
+ * @param {string|string[]} value The CSS/attribute value to escape.
25
+ * @returns {string} Escaped value safe for use in SVG attributes.
26
+ */
27
+ const escapeCSSValue = (value) => {
28
+ // Convert non-string values (e.g., arrays for gradients) to string first
29
+ const strValue = typeof value === "string" ? value : String(value);
30
+
31
+ // Escape quotes and special characters that could break out of attribute context
32
+ return strValue
33
+ .replace(/\\/g, "\\\\") // Escape backslashes first
34
+ .replace(/"/g, '\\"') // Escape double quotes
35
+ .replace(/'/g, "\\'") // Escape single quotes
36
+ .replace(/\n/g, "\\A ") // Escape newlines
37
+ .replace(/\r/g, "") // Remove carriage returns
38
+ .replace(/\f/g, "") // Remove form feeds
39
+ .replace(/</g, "\\3C ") // Escape less-than
40
+ .replace(/>/g, "\\3E "); // Escape greater-than
41
+ };
42
+
43
+ export { encodeHTML, escapeCSSValue };
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+
3
+ import axios from "axios";
4
+
5
+ /**
6
+ * Send GraphQL request to GitHub API.
7
+ *
8
+ * @param {import('axios').AxiosRequestConfig['data']} data Request data.
9
+ * @param {import('axios').AxiosRequestConfig['headers']} headers Request headers.
10
+ * @returns {Promise<any>} Request response.
11
+ */
12
+ const request = (data, headers) => {
13
+ return axios({
14
+ url: "https://api.github.com/graphql",
15
+ method: "post",
16
+ headers: {
17
+ "User-Agent": "github-readme-stats",
18
+ ...headers,
19
+ },
20
+ data,
21
+ });
22
+ };
23
+
24
+ export { request };
@@ -0,0 +1,63 @@
1
+ // @ts-check
2
+
3
+ import escapeHtml from "escape-html";
4
+
5
+ const icons = {
6
+ star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
7
+ commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
8
+ prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
9
+ prs_merged: `<path fill-rule="evenodd" d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z" />`,
10
+ prs_merged_percentage: `<path fill-rule="evenodd" d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />`,
11
+ issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
12
+ icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
13
+ contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
14
+ fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
15
+ reviews: `<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>`,
16
+ discussions_started: `<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z" />`,
17
+ discussions_answered: `<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />`,
18
+ gist: `<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />`,
19
+ };
20
+
21
+ /**
22
+ * Get rank icon
23
+ *
24
+ * **Security Note:** This function safely handles untrusted input by HTML-encoding
25
+ * the `rankLevel` parameter. Data from external APIs (like GitHub) is sanitized
26
+ * before being inserted into the SVG.
27
+ *
28
+ * @param {string} rankIcon - The rank icon type.
29
+ * @param {string} rankLevel - The rank level (will be HTML-encoded).
30
+ * @param {number} percentile - The rank percentile.
31
+ * @returns {string} - The SVG code of the rank icon
32
+ */
33
+ const rankIcon = (rankIcon, rankLevel, percentile) => {
34
+ switch (rankIcon) {
35
+ case "github":
36
+ return `
37
+ <svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
38
+ <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
39
+ </svg>
40
+ `;
41
+ case "percentile":
42
+ // percentile.toFixed(1) produces numeric strings (e.g., "99.5"), safe to use directly
43
+ return `
44
+ <text x="-5" y="-12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-top-header" class="rank-percentile-header">
45
+ Top
46
+ </text>
47
+ <text x="-5" y="12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-rank-value" class="rank-percentile-text">
48
+ ${percentile.toFixed(1)}%
49
+ </text>
50
+ `;
51
+ case "default":
52
+ default:
53
+ // Sanitize rankLevel from external API to prevent XSS
54
+ return `
55
+ <text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
56
+ ${escapeHtml(rankLevel || "")}
57
+ </text>
58
+ `;
59
+ }
60
+ };
61
+
62
+ export { icons, rankIcon };
63
+ export default icons;
@@ -0,0 +1,13 @@
1
+ // @ts-check
2
+
3
+ export { blacklist } from "./blacklist.js";
4
+ export { Card } from "./Card.js";
5
+ export { I18n } from "./I18n.js";
6
+ export { icons } from "./icons.js";
7
+ export { retryer } from "./retryer.js";
8
+ export {
9
+ ERROR_CARD_LENGTH,
10
+ renderError,
11
+ flexLayout,
12
+ measureText,
13
+ } from "./render.js";