@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,294 @@
1
+ // @ts-check
2
+
3
+ import { encodeHTML, escapeCSSValue } from "./html.js";
4
+ import { flexLayout } from "./render.js";
5
+
6
+ class Card {
7
+ /**
8
+ * Creates a new card instance.
9
+ *
10
+ * @param {object} args Card arguments.
11
+ * @param {number=} args.width Card width.
12
+ * @param {number=} args.height Card height.
13
+ * @param {number=} args.border_radius Card border radius.
14
+ * @param {string=} args.customTitle Card custom title.
15
+ * @param {string=} args.defaultTitle Card default title.
16
+ * @param {string=} args.titlePrefixIcon Card title prefix icon.
17
+ * @param {object} [args.colors={}] Card colors arguments.
18
+ * @param {string=} args.colors.titleColor Card title color.
19
+ * @param {string=} args.colors.textColor Card text color.
20
+ * @param {string=} args.colors.iconColor Card icon color.
21
+ * @param {string|string[]=} args.colors.bgColor Card background color.
22
+ * @param {string=} args.colors.borderColor Card border color.
23
+ */
24
+ constructor({
25
+ width = 100,
26
+ height = 100,
27
+ border_radius = 4.5,
28
+ colors = {},
29
+ customTitle,
30
+ defaultTitle = "",
31
+ titlePrefixIcon,
32
+ }) {
33
+ this.width = width;
34
+ this.height = height;
35
+
36
+ this.hideBorder = false;
37
+ this.hideTitle = false;
38
+
39
+ this.border_radius = border_radius;
40
+
41
+ // returns theme based colors with proper overrides and defaults
42
+ this.colors = colors;
43
+ this.title =
44
+ customTitle === undefined
45
+ ? encodeHTML(defaultTitle)
46
+ : encodeHTML(customTitle);
47
+
48
+ this.css = "";
49
+
50
+ this.paddingX = 25;
51
+ this.paddingY = 35;
52
+ this.titlePrefixIcon = titlePrefixIcon;
53
+ this.animations = true;
54
+ this.a11yTitle = "";
55
+ this.a11yDesc = "";
56
+ }
57
+
58
+ /**
59
+ * @returns {void}
60
+ */
61
+ disableAnimations() {
62
+ this.animations = false;
63
+ }
64
+
65
+ /**
66
+ * @param {Object} props The props object.
67
+ * @param {string} props.title Accessibility title.
68
+ * @param {string} props.desc Accessibility description.
69
+ * @returns {void}
70
+ */
71
+ setAccessibilityLabel({ title, desc }) {
72
+ this.a11yTitle = title;
73
+ this.a11yDesc = desc;
74
+ }
75
+
76
+ /**
77
+ * @param {string} value The CSS to add to the card.
78
+ * @returns {void}
79
+ */
80
+ setCSS(value) {
81
+ this.css = value;
82
+ }
83
+
84
+ /**
85
+ * @param {boolean} value Whether to hide the border or not.
86
+ * @returns {void}
87
+ */
88
+ setHideBorder(value) {
89
+ this.hideBorder = value;
90
+ }
91
+
92
+ /**
93
+ * @param {boolean} value Whether to hide the title or not.
94
+ * @returns {void}
95
+ */
96
+ setHideTitle(value) {
97
+ this.hideTitle = value;
98
+ if (value) {
99
+ this.height -= 30;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {string} text The title to set.
105
+ * @returns {void}
106
+ */
107
+ setTitle(text) {
108
+ this.title = text;
109
+ }
110
+
111
+ /**
112
+ * @returns {string} The rendered card title.
113
+ */
114
+ renderTitle() {
115
+ const titleText = `
116
+ <text
117
+ x="0"
118
+ y="0"
119
+ class="header"
120
+ data-testid="header"
121
+ >${this.title}</text>
122
+ `;
123
+
124
+ const prefixIcon = `
125
+ <svg
126
+ class="icon"
127
+ x="0"
128
+ y="-13"
129
+ viewBox="0 0 16 16"
130
+ version="1.1"
131
+ width="16"
132
+ height="16"
133
+ >
134
+ ${this.titlePrefixIcon}
135
+ </svg>
136
+ `;
137
+ return `
138
+ <g
139
+ data-testid="card-title"
140
+ transform="translate(${this.paddingX}, ${this.paddingY})"
141
+ >
142
+ ${flexLayout({
143
+ items: [this.titlePrefixIcon ? prefixIcon : "", titleText],
144
+ gap: 25,
145
+ }).join("")}
146
+ </g>
147
+ `;
148
+ }
149
+
150
+ /**
151
+ * @returns {string} The rendered card gradient.
152
+ */
153
+ renderGradient() {
154
+ if (typeof this.colors.bgColor !== "object") {
155
+ return "";
156
+ }
157
+
158
+ const gradients = this.colors.bgColor.slice(1);
159
+ return typeof this.colors.bgColor === "object"
160
+ ? `
161
+ <defs>
162
+ <linearGradient
163
+ id="gradient"
164
+ gradientTransform="rotate(${this.colors.bgColor[0]})"
165
+ gradientUnits="userSpaceOnUse"
166
+ >
167
+ ${gradients.map((grad, index) => {
168
+ let offset = (index * 100) / (gradients.length - 1);
169
+ return `<stop offset="${offset}%" stop-color="#${grad}" />`;
170
+ })}
171
+ </linearGradient>
172
+ </defs>
173
+ `
174
+ : "";
175
+ }
176
+
177
+ /**
178
+ * Retrieves css animations for a card.
179
+ *
180
+ * @returns {string} Animation css.
181
+ */
182
+ getAnimations = () => {
183
+ return `
184
+ /* Animations */
185
+ @keyframes scaleInAnimation {
186
+ from {
187
+ transform: translate(-5px, 5px) scale(0);
188
+ }
189
+ to {
190
+ transform: translate(-5px, 5px) scale(1);
191
+ }
192
+ }
193
+ @keyframes fadeInAnimation {
194
+ from {
195
+ opacity: 0;
196
+ }
197
+ to {
198
+ opacity: 1;
199
+ }
200
+ }
201
+ `;
202
+ };
203
+
204
+ /**
205
+ * Renders the card with inner body HTML/SVG.
206
+ *
207
+ * **Security Model:**
208
+ * This method accepts pre-built SVG content from internal rendering functions.
209
+ * All user-controlled data (custom_title, descriptions, language names, etc.)
210
+ * MUST be sanitized using `escapeHtml()` or `escapeCSSValue()` BEFORE being
211
+ * included in the body content. The sanitization happens at the data entry points
212
+ * in the individual card rendering functions (stats.js, repo.js, gist.js, etc.),
213
+ * not at this aggregation point.
214
+ *
215
+ * This is a trusted internal API - the body parameter contains SVG elements
216
+ * that have already been constructed with proper escaping of dynamic values.
217
+ *
218
+ * @param {string} body The inner body of the card (pre-sanitized SVG content).
219
+ * @returns {string} The rendered card.
220
+ */
221
+ render(body) {
222
+ // Security: body contains pre-sanitized SVG from internal render functions.
223
+ // User inputs are escaped at source using escapeHtml() before reaching here.
224
+ // Sanitize color values to prevent XSS in SVG attributes
225
+ const safeTitleColor = escapeCSSValue(this.colors.titleColor || "");
226
+ const safeBorderColor = escapeCSSValue(this.colors.borderColor || "");
227
+ const safeBgColor =
228
+ typeof this.colors.bgColor === "object"
229
+ ? "url(#gradient)"
230
+ : escapeCSSValue(this.colors.bgColor || "");
231
+
232
+ return `
233
+ <svg
234
+ width="${this.width}"
235
+ height="${this.height}"
236
+ viewBox="0 0 ${this.width} ${this.height}"
237
+ fill="none"
238
+ xmlns="http://www.w3.org/2000/svg"
239
+ role="img"
240
+ aria-labelledby="descId"
241
+ >
242
+ <title id="titleId">${encodeHTML(this.a11yTitle)}</title>
243
+ <desc id="descId">${encodeHTML(this.a11yDesc)}</desc>
244
+ <style>
245
+ .header {
246
+ font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
247
+ fill: ${safeTitleColor};
248
+ animation: fadeInAnimation 0.8s ease-in-out forwards;
249
+ }
250
+ @supports(-moz-appearance: auto) {
251
+ /* Selector detects Firefox */
252
+ .header { font-size: 15.5px; }
253
+ }
254
+ ${this.css}
255
+
256
+ ${process.env.NODE_ENV === "test" ? "" : this.getAnimations()}
257
+ ${
258
+ this.animations === false
259
+ ? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
260
+ : ""
261
+ }
262
+ </style>
263
+
264
+ ${this.renderGradient()}
265
+
266
+ <rect
267
+ data-testid="card-bg"
268
+ x="0.5"
269
+ y="0.5"
270
+ rx="${this.border_radius}"
271
+ height="99%"
272
+ stroke="${safeBorderColor}"
273
+ width="${this.width - 1}"
274
+ fill="${safeBgColor}"
275
+ stroke-opacity="${this.hideBorder ? 0 : 1}"
276
+ />
277
+
278
+ ${this.hideTitle ? "" : this.renderTitle()}
279
+
280
+ <g
281
+ data-testid="main-card-body"
282
+ transform="translate(0, ${
283
+ this.hideTitle ? this.paddingX : this.paddingY + 20
284
+ })"
285
+ >
286
+ ${body}
287
+ </g>
288
+ </svg>
289
+ `;
290
+ }
291
+ }
292
+
293
+ export { Card };
294
+ export default Card;
@@ -0,0 +1,41 @@
1
+ // @ts-check
2
+
3
+ const FALLBACK_LOCALE = "en";
4
+
5
+ /**
6
+ * I18n translation class.
7
+ */
8
+ class I18n {
9
+ /**
10
+ * Constructor.
11
+ *
12
+ * @param {Object} options Options.
13
+ * @param {string=} options.locale Locale.
14
+ * @param {any} options.translations Translations.
15
+ */
16
+ constructor({ locale, translations }) {
17
+ this.locale = locale || FALLBACK_LOCALE;
18
+ this.translations = translations;
19
+ }
20
+
21
+ /**
22
+ * Get translation.
23
+ *
24
+ * @param {string} str String to translate.
25
+ * @returns {string} Translated string.
26
+ */
27
+ t(str) {
28
+ if (!this.translations[str]) {
29
+ throw new Error(`${str} Translation string not found`);
30
+ }
31
+
32
+ if (!this.translations[str][this.locale]) {
33
+ throw new Error(`'${str}' translation not found for requested locale'`);
34
+ }
35
+
36
+ return this.translations[str][this.locale];
37
+ }
38
+ }
39
+
40
+ export { I18n };
41
+ export default I18n;
@@ -0,0 +1,69 @@
1
+ // @ts-check
2
+
3
+ import { renderError } from "./render.js";
4
+ import { blacklist } from "./blacklist.js";
5
+ import { whitelist, gistWhitelist } from "./envs.js";
6
+
7
+ const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted";
8
+ const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted";
9
+ const BLACKLISTED_MESSAGE = "This username is blacklisted";
10
+
11
+ /**
12
+ * Guards access using whitelist/blacklist.
13
+ *
14
+ * @param {Object} args The parameters object.
15
+ * @param {any} args.res The response object.
16
+ * @param {string} args.id Resource identifier (username or gist id).
17
+ * @param {"username"|"gist"|"wakatime"} args.type The type of identifier.
18
+ * @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card.
19
+ * @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure.
20
+ */
21
+ const guardAccess = ({ res, id, type, colors }) => {
22
+ if (!["username", "gist", "wakatime"].includes(type)) {
23
+ throw new Error(
24
+ 'Invalid type. Expected "username", "gist", or "wakatime".',
25
+ );
26
+ }
27
+
28
+ const currentWhitelist = type === "gist" ? gistWhitelist : whitelist;
29
+ const notWhitelistedMsg =
30
+ type === "gist"
31
+ ? NOT_WHITELISTED_GIST_MESSAGE
32
+ : NOT_WHITELISTED_USERNAME_MESSAGE;
33
+
34
+ if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) {
35
+ const result = res.send(
36
+ renderError({
37
+ message: notWhitelistedMsg,
38
+ secondaryMessage: "Please deploy your own instance",
39
+ renderOptions: {
40
+ ...colors,
41
+ show_repo_link: false,
42
+ },
43
+ }),
44
+ );
45
+ return { isPassed: false, result };
46
+ }
47
+
48
+ if (
49
+ type === "username" &&
50
+ currentWhitelist === undefined &&
51
+ blacklist.includes(id)
52
+ ) {
53
+ const result = res.send(
54
+ renderError({
55
+ message: BLACKLISTED_MESSAGE,
56
+ secondaryMessage: "Please deploy your own instance",
57
+ renderOptions: {
58
+ ...colors,
59
+ show_repo_link: false,
60
+ },
61
+ }),
62
+ );
63
+ return { isPassed: false, result };
64
+ }
65
+
66
+ return { isPassed: true };
67
+ };
68
+
69
+ export { guardAccess };
@@ -0,0 +1,221 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @file Shared API utilities for request handling, validation, and error responses.
5
+ * Centralizes common patterns to reduce code duplication and improve maintainability.
6
+ */
7
+
8
+ import { renderError } from "./render.js";
9
+ import { validateColor, validateTheme } from "./color.js";
10
+ import { MissingParamError, retrieveSecondaryMessage } from "./error.js";
11
+ import { setErrorCacheHeaders } from "./cache.js";
12
+
13
+ /**
14
+ * @typedef {Object} ColorOptions
15
+ * @property {string|undefined} title_color
16
+ * @property {string|undefined} text_color
17
+ * @property {string|undefined} bg_color
18
+ * @property {string|undefined} border_color
19
+ * @property {string|undefined} theme
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} RawColorParams
24
+ * @property {string} [title_color]
25
+ * @property {string} [text_color]
26
+ * @property {string} [bg_color]
27
+ * @property {string} [border_color]
28
+ * @property {string} [theme]
29
+ */
30
+
31
+ /**
32
+ * Creates validated color options from raw query parameters.
33
+ * All color values are validated and sanitized to prevent XSS attacks.
34
+ *
35
+ * @param {RawColorParams} params - Raw color parameters from query string.
36
+ * @returns {ColorOptions} Validated and sanitized color options.
37
+ */
38
+ const createValidatedColorOptions = ({
39
+ title_color,
40
+ text_color,
41
+ bg_color,
42
+ border_color,
43
+ theme,
44
+ }) => ({
45
+ title_color: validateColor(title_color),
46
+ text_color: validateColor(text_color),
47
+ bg_color: validateColor(bg_color),
48
+ border_color: validateColor(border_color),
49
+ theme: validateTheme(theme),
50
+ });
51
+
52
+ /**
53
+ * @typedef {Object} ErrorResponseOptions
54
+ * @property {any} res - Express response object.
55
+ * @property {Error|unknown} error - The error that occurred.
56
+ * @property {ColorOptions} colorOptions - Validated color options for error card styling.
57
+ */
58
+
59
+ /**
60
+ * Patterns that indicate error messages containing user-controlled data.
61
+ * These patterns are replaced with safe generic alternatives to prevent XSS.
62
+ * @type {ReadonlyArray<{pattern: RegExp, replacement: string}>}
63
+ */
64
+ const UNSAFE_MESSAGE_PATTERNS = [
65
+ {
66
+ pattern: /translation not found for/i,
67
+ replacement: "Invalid locale specified",
68
+ },
69
+ {
70
+ pattern: /Could not resolve to a User with the login of/i,
71
+ replacement: "User not found",
72
+ },
73
+ ];
74
+
75
+ /**
76
+ * Sanitizes error messages to prevent XSS by filtering out messages that
77
+ * contain user-controlled data (like usernames or locales embedded in errors).
78
+ * Other error messages in this codebase are hardcoded and safe.
79
+ * Note: renderError also applies HTML encoding as an additional safety layer.
80
+ *
81
+ * @param {string} message - The error message to sanitize.
82
+ * @returns {string} A safe error message.
83
+ */
84
+ const sanitizeErrorMessage = (message) => {
85
+ if (!message || typeof message !== "string") {
86
+ return "An error occurred";
87
+ }
88
+
89
+ // Replace messages containing user-controlled data with safe alternatives
90
+ for (const { pattern, replacement } of UNSAFE_MESSAGE_PATTERNS) {
91
+ if (pattern.test(message)) {
92
+ return replacement;
93
+ }
94
+ }
95
+
96
+ // Other error messages in this codebase are hardcoded strings (safe)
97
+ // renderError will HTML-encode the output as an additional safety layer
98
+ return message;
99
+ };
100
+
101
+ /**
102
+ * Handles API errors by setting cache headers and sending a rendered error response.
103
+ * Centralizes error handling logic to ensure consistent behavior across all API endpoints.
104
+ *
105
+ * @param {ErrorResponseOptions} options - Error handling options.
106
+ * @returns {any} The response result.
107
+ */
108
+ const handleApiError = ({ res, error, colorOptions }) => {
109
+ setErrorCacheHeaders(res);
110
+
111
+ if (error instanceof Error) {
112
+ // Sanitize error message to prevent XSS from user-controlled data in exceptions
113
+ const safeMessage = sanitizeErrorMessage(error.message);
114
+ const rawSecondary = retrieveSecondaryMessage(error);
115
+ const safeSecondary = rawSecondary
116
+ ? sanitizeErrorMessage(rawSecondary)
117
+ : undefined;
118
+
119
+ return res.send(
120
+ renderError({
121
+ message: safeMessage,
122
+ secondaryMessage: safeSecondary,
123
+ renderOptions: {
124
+ ...colorOptions,
125
+ show_repo_link: !(error instanceof MissingParamError),
126
+ },
127
+ }),
128
+ );
129
+ }
130
+
131
+ return res.send(
132
+ renderError({
133
+ message: "An unknown error occurred",
134
+ renderOptions: colorOptions,
135
+ }),
136
+ );
137
+ };
138
+
139
+ /**
140
+ * @typedef {Object} ValidationErrorOptions
141
+ * @property {any} res - Express response object.
142
+ * @property {string} message - Primary error message.
143
+ * @property {string} [secondaryMessage] - Secondary error message.
144
+ * @property {ColorOptions} colorOptions - Validated color options for error card styling.
145
+ */
146
+
147
+ /**
148
+ * Sends a validation error response with consistent styling.
149
+ * Used for parameter validation failures before main processing.
150
+ *
151
+ * @param {ValidationErrorOptions} options - Validation error options.
152
+ * @returns {any} The response result.
153
+ */
154
+ const sendValidationError = ({
155
+ res,
156
+ message,
157
+ secondaryMessage = "",
158
+ colorOptions,
159
+ }) => {
160
+ return res.send(
161
+ renderError({
162
+ message,
163
+ secondaryMessage,
164
+ renderOptions: colorOptions,
165
+ }),
166
+ );
167
+ };
168
+
169
+ /**
170
+ * Sets the standard SVG content type header.
171
+ *
172
+ * @param {any} res - Express response object.
173
+ */
174
+ const setSvgContentType = (res) => {
175
+ res.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
176
+ };
177
+
178
+ /**
179
+ * Sets the standard JSON content type header.
180
+ *
181
+ * @param {any} res - Express response object.
182
+ */
183
+ const setJsonContentType = (res) => {
184
+ res.setHeader("Content-Type", "application/json");
185
+ };
186
+
187
+ /**
188
+ * Parses and validates a numeric parameter with bounds checking.
189
+ *
190
+ * @param {string|undefined} value - The value to parse.
191
+ * @param {number|undefined} defaultValue - Default value if parsing fails.
192
+ * @param {number} [min] - Minimum allowed value.
193
+ * @param {number} [max] - Maximum allowed value.
194
+ * @returns {number|undefined} The parsed and clamped value.
195
+ */
196
+ const parseNumericParam = (value, defaultValue, min, max) => {
197
+ if (value === undefined || value === null) {
198
+ return defaultValue;
199
+ }
200
+ const parsed = parseFloat(value);
201
+ if (isNaN(parsed)) {
202
+ return defaultValue;
203
+ }
204
+ let result = parsed;
205
+ if (min !== undefined) {
206
+ result = Math.max(min, result);
207
+ }
208
+ if (max !== undefined) {
209
+ result = Math.min(max, result);
210
+ }
211
+ return result;
212
+ };
213
+
214
+ export {
215
+ createValidatedColorOptions,
216
+ handleApiError,
217
+ sendValidationError,
218
+ setSvgContentType,
219
+ setJsonContentType,
220
+ parseNumericParam,
221
+ };
@@ -0,0 +1,10 @@
1
+ const blacklist = [
2
+ "renovate-bot",
3
+ "technote-space",
4
+ "sw-yx",
5
+ "YourUsername",
6
+ "[YourUsername]",
7
+ ];
8
+
9
+ export { blacklist };
10
+ export default blacklist;