@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.
- package/LICENSE +21 -0
- package/api/gist.js +98 -0
- package/api/index.js +146 -0
- package/api/pin.js +114 -0
- package/api/status/pat-info.js +193 -0
- package/api/status/up.js +129 -0
- package/api/top-langs.js +163 -0
- package/api/wakatime.js +129 -0
- package/package.json +102 -0
- package/readme.md +412 -0
- package/src/calculateRank.js +87 -0
- package/src/cards/gist.js +151 -0
- package/src/cards/index.js +4 -0
- package/src/cards/repo.js +199 -0
- package/src/cards/stats.js +607 -0
- package/src/cards/top-languages.js +968 -0
- package/src/cards/types.d.ts +67 -0
- package/src/cards/wakatime.js +482 -0
- package/src/common/Card.js +294 -0
- package/src/common/I18n.js +41 -0
- package/src/common/access.js +69 -0
- package/src/common/api-utils.js +221 -0
- package/src/common/blacklist.js +10 -0
- package/src/common/cache.js +153 -0
- package/src/common/color.js +194 -0
- package/src/common/envs.js +15 -0
- package/src/common/error.js +84 -0
- package/src/common/fmt.js +90 -0
- package/src/common/html.js +43 -0
- package/src/common/http.js +24 -0
- package/src/common/icons.js +63 -0
- package/src/common/index.js +13 -0
- package/src/common/languageColors.json +651 -0
- package/src/common/log.js +14 -0
- package/src/common/ops.js +124 -0
- package/src/common/render.js +261 -0
- package/src/common/retryer.js +97 -0
- package/src/common/worker-adapter.js +148 -0
- package/src/common/worker-env.js +48 -0
- package/src/fetchers/gist.js +114 -0
- package/src/fetchers/repo.js +118 -0
- package/src/fetchers/stats.js +350 -0
- package/src/fetchers/top-languages.js +192 -0
- package/src/fetchers/types.d.ts +118 -0
- package/src/fetchers/wakatime.js +109 -0
- package/src/index.js +2 -0
- package/src/translations.js +1105 -0
- package/src/worker.ts +116 -0
- package/themes/README.md +229 -0
- 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";
|