@fajarkim/github-readme-profile
Version:
🙀 Generate dynamic GitHub stats cards in SVG format. Fast, customizable, and ready to embed in your profile README.
164 lines (151 loc) • 7.15 kB
text/typescript
import escapeHTML from "escape-html";
import { Resvg } from "@resvg/resvg-js";
import parseBoolean from "@barudakrosul/parse-boolean";
import { getData,GetData } from "../src/getData";
import card from "../src/card";
import { themes, Themes } from "../themes/index";
import { isValidHexColor, isValidGradient } from "../src/common/utils";
/**
* Type representing the configuration for the card options.
*
* @typedef {Object} UiConfig
* @property {string} titleColor - Color for the title text.
* @property {string} textColor - Color for the main text.
* @property {string} iconColor - Color for icons.
* @property {string} borderColor - Color for borders.
* @property {string} strokeColor - Color for strokes.
* @property {string} usernameColor - Color for the username.
* @property {string|string[]} bgColor - Background color or gradient.
* @property {string} Title - Add custom title (optional).
* @property {string} Locale - Locale setting.
* @property {number|string} borderWidth - Width of borders.
* @property {number|string} borderRadius - Radius of borders.
* @property {boolean|string} disabledAnimations - Toggle for disabling animations.
* @property {string} Format - Output format (e.g., "svg", "png", or "json").
* @property {string|undefined} hiddenItems - Items to hide.
* @property {string|undefined} showItems - Items to show.
* @property {boolean|string} hideStroke - Toggle for hiding strokes.
* @property {boolean|string} hideBorder - Toggle for hiding borders.
* @property {boolean|string} Revert - Invert display order, stats to left and image to right.
* @property {number|string} photoQuality - Photo image quality.
* @property {number|string} photoResize - Photo image resize.
*/
type UiConfig = {
titleColor: string;
textColor: string;
iconColor: string;
borderColor: string;
strokeColor: string;
usernameColor: string;
bgColor: string|string[];
Title: string | undefined;
Locale: string;
borderWidth: number | string;
borderRadius: number | string;
disabledAnimations: boolean | string;
Format: string;
hiddenItems: string | undefined;
showItems: string | undefined;
hideStroke: boolean | string;
hideBorder: boolean | string;
Revert: boolean | string;
photoQuality: number | string;
photoResize: number | string;
};
/**
* Generates an XML string representation of the provided data.
*
* @param {any} data - The data to be converted into XML format.
* @returns {string} - A string containing the XML representation of the data.
*/
function generateXML(data: any): string {
let xml = `<stats>\n`;
for (const key in data) {
xml += ` <${key}>${escapeHTML(data[key])}</${key}>\n`;
}
xml += `</stats>`;
return xml;
}
/**
* Handles the generation card of a GitHub stats based on user data and specified options.
*
* @param {any} req - The request object from the client.
* @param {any} res - The response object to send data back to the client.
* @returns {Promise<void>} - A promise that resolves when the photo profile is generated and sent.
*/
async function readmeStats(req: any, res: any): Promise<void> {
try {
const username = escapeHTML(req.query.username);
const photoQuality = Math.max(0, Math.min(parseInt(escapeHTML(req.query.photo_quality || "15")), 100));
const photoResize = Math.max(10, parseInt(escapeHTML(req.query.photo_resize || "150")));
const fallbackTheme = "default";
const defaultTheme: Themes[keyof Themes] = themes[fallbackTheme];
const selectTheme: Themes[keyof Themes] = themes[req.query.theme] || defaultTheme;
const uiConfig: UiConfig = {
titleColor: escapeHTML(req.query.title_color || selectTheme.title_color || defaultTheme.title_color),
textColor: escapeHTML(req.query.text_color || selectTheme.text_color || defaultTheme.text_color),
iconColor: escapeHTML(req.query.icon_color || selectTheme.icon_color || defaultTheme.icon_color),
borderColor: escapeHTML(req.query.border_color || selectTheme.border_color || defaultTheme.border_color),
strokeColor: escapeHTML(req.query.stroke_color || req.query.border_color || selectTheme.stroke_color || selectTheme.border_color || defaultTheme.border_color),
usernameColor: escapeHTML(req.query.username_color || req.query.text_color || selectTheme.username_color || selectTheme.text_color || defaultTheme.text_color),
bgColor: escapeHTML(req.query.bg_color || selectTheme.bg_color || defaultTheme.bg_color),
Title: escapeHTML(req.query.title),
Locale: escapeHTML(req.query.locale || "en"),
borderWidth: escapeHTML(req.query.border_width || 1),
borderRadius: escapeHTML(req.query.border_radius || 4.5),
disabledAnimations: parseBoolean(escapeHTML(req.query.disabled_animations)) || false,
Format: escapeHTML(req.query.format || "svg"),
hiddenItems: escapeHTML(req.query.hide),
showItems: escapeHTML(req.query.show),
hideStroke: parseBoolean(escapeHTML(req.query.hide_stroke)) || false,
hideBorder: parseBoolean(escapeHTML(req.query.hide_border)) || false,
Revert: parseBoolean(escapeHTML(req.query.revert)) || false,
photoQuality: photoQuality,
photoResize: photoResize,
};
if (!username) {
throw new Error("Username is required");
}
if (
!isValidHexColor(uiConfig.titleColor) ||
!isValidHexColor(uiConfig.textColor) ||
!isValidHexColor(uiConfig.iconColor) ||
!isValidHexColor(uiConfig.borderColor) ||
!isValidHexColor(uiConfig.usernameColor) ||
!isValidHexColor(uiConfig.strokeColor)
) {
throw new Error("Enter a valid hex color code");
}
if (!isValidGradient(uiConfig.bgColor)) {
if (!isValidHexColor(uiConfig.bgColor)) {
throw new Error("Enter a valid hex color code");
}
}
const fetchStats: GetData = await getData(username);
res.setHeader("Cache-Control", "s-maxage=7200, stale-while-revalidate");
if (uiConfig.Format === "json") {
fetchStats.picture = `data:image/png;base64,${fetchStats.picture}`;
res.json(fetchStats);
} else if (uiConfig.Format === "xml") {
fetchStats.picture = `data:image/png;base64,${fetchStats.picture}`;
const xmlData = generateXML(fetchStats);
res.setHeader("Content-Type", "application/xml");
res.send(xmlData);
} else if (uiConfig.Format === "png") {
const svgString = await card(fetchStats, uiConfig);
const resvg = new Resvg(svgString, { font: { defaultFontFamily: "Segoe UI" }});
const pngBuffer = await resvg.render().asPng();
res.setHeader("Content-Type", "image/png");
res.send(pngBuffer);
} else {
res.setHeader("Content-Type", "image/svg+xml");
const svg = await card(fetchStats, uiConfig);
res.send(svg);
}
} catch (error: any) {
const message = error.message;
res.status(500).send(escapeHTML(message));
}
}
export { UiConfig, readmeStats };
export default readmeStats;