UNPKG

@zo-bro-23/github-readme-stats-test

Version:

Dynamically generate stats for your GitHub readme

337 lines (306 loc) 9.36 kB
// @ts-check import { Card } from "../common/Card.js"; import { createProgressNode } from "../common/createProgressNode.js"; import { I18n } from "../common/I18n.js"; import { clampValue, flexLayout, getCardColors, lowercaseTrim, } from "../common/utils.js"; import { getStyles } from "../getStyles.js"; import { wakatimeCardLocales } from "../translations.js"; /** Import language colors. * * @description Here we use the workaround found in * https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node * since vercel is using v16.14.0 which does not yet support json imports without the * --experimental-json-modules flag. */ import { createRequire } from "module"; const require = createRequire(import.meta.url); const languageColors = require("../common/languageColors.json"); // now works /** * Creates the no coding activity SVG node. * * @param {{color: string, text: string}} The function prop */ const noCodingActivityNode = ({ color, text }) => { return ` <text x="25" y="11" class="stat bold" fill="${color}">${text}</text> `; }; /** * Create compact WakaTime layout. * * @param {Object[]} args The function arguments. * @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array. * @param {number} totalSize The total size of the languages. * @param {number} x The x position of the language node. * @param {number} y The y position of the language node. */ const createCompactLangNode = ({ lang, totalSize, x, y }) => { const color = languageColors[lang.name] || "#858585"; return ` <g transform="translate(${x}, ${y})"> <circle cx="5" cy="6" r="5" fill="${color}" /> <text data-testid="lang-name" x="15" y="10" class='lang-name'> ${lang.name} - ${lang.text} </text> </g> `; }; /** * Create WakaTime language text node item. * * @param {Object[]} args The function arguments. * @param {import("../fetchers/types").WakaTimeLang} lang The language object. * @param {number} totalSize The total size of the languages. * @param {number} x The x position of the language node. * @param {number} y The y position of the language node. */ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { return langs.map((lang, index) => { if (index % 2 === 0) { return createCompactLangNode({ lang, x: 25, y: 12.5 * index + y, totalSize, }); } return createCompactLangNode({ lang, x: 230, y: 12.5 + 12.5 * index, totalSize, }); }); }; /** * Create WakaTime text item. * * @param {Object[]} args The function arguments. * @param {string} id The id of the text node item. * @param {string} label The label of the text node item. * @param {string} value The value of the text node item. * @param {number} index The index of the text node item. * @param {percent} percent Percentage of the text node item. * @param {boolean} hideProgress Whether to hide the progress bar. * @param {string} progressBarBackgroundColor The color of the progress bar background. */ const createTextNode = ({ id, label, value, index, percent, hideProgress, progressBarColor, progressBarBackgroundColor, }) => { const staggerDelay = (index + 3) * 150; const cardProgress = hideProgress ? null : createProgressNode({ x: 110, y: 4, progress: percent, color: progressBarColor, width: 220, // @ts-ignore name: label, progressBarBackgroundColor, }); return ` <g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)"> <text class="stat bold" y="12.5" data-testid="${id}">${label}:</text> <text class="stat" x="${hideProgress ? 170 : 350}" y="12.5" >${value}</text> ${cardProgress} </g> `; }; /** * Recalculating percentages so that, compact layout's progress bar does not break when * hiding languages. * * @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array. * @return {import("../fetchers/types").WakaTimeLang[]} The recalculated languages array. */ const recalculatePercentages = (languages) => { const totalSum = languages.reduce( (totalSum, language) => totalSum + language.percent, 0, ); const weight = +(100 / totalSum).toFixed(2); languages.forEach((language) => { language.percent = +(language.percent * weight).toFixed(2); }); }; /** * Renders WakaTime card. * * @param {Partial<import('../fetchers/types').WakaTimeData>} stats WakaTime stats. * @param {Partial<import('./types').WakaTimeOptions>} options Card options. * @returns {string} WakaTime card SVG. */ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { let { languages = [] } = stats; const { hide_title = false, hide_border = false, hide, line_height = 25, title_color, icon_color, text_color, bg_color, theme = "default", hide_progress, custom_title, locale, layout, langs_count = languages.length, border_radius, border_color, } = options; const shouldHideLangs = Array.isArray(hide) && hide.length > 0; if (shouldHideLangs) { const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang))); languages = languages.filter( (lang) => !languagesToHide.has(lowercaseTrim(lang.name)), ); } // Since the percentages are sorted in descending order, we can just // slice from the beginning without sorting. languages = languages.slice(0, langs_count); recalculatePercentages(languages); const i18n = new I18n({ locale, translations: wakatimeCardLocales, }); const lheight = parseInt(String(line_height), 10); const langsCount = clampValue(parseInt(String(langs_count)), 1, langs_count); // returns theme based colors with proper overrides and defaults const { titleColor, textColor, iconColor, bgColor, borderColor } = getCardColors({ title_color, icon_color, text_color, bg_color, border_color, theme, }); const filteredLanguages = languages .filter((language) => language.hours || language.minutes) .slice(0, langsCount); // Calculate the card height depending on how many items there are // but if rank circle is visible clamp the minimum height to `150` let height = Math.max(45 + (filteredLanguages.length + 1) * lheight, 150); const cssStyles = getStyles({ titleColor, textColor, iconColor, }); let finalLayout = ""; let width = 440; // RENDER COMPACT LAYOUT if (layout === "compact") { width = width + 50; height = 90 + Math.round(filteredLanguages.length / 2) * 25; // progressOffset holds the previous language's width and used to offset the next language // so that we can stack them one after another, like this: [--][----][---] let progressOffset = 0; const compactProgressBar = filteredLanguages .map((language) => { // const progress = (width * lang.percent) / 100; const progress = ((width - 25) * language.percent) / 100; const languageColor = languageColors[language.name] || "#858585"; const output = ` <rect mask="url(#rect-mask)" data-testid="lang-progress" x="${progressOffset}" y="0" width="${progress}" height="8" fill="${languageColor}" /> `; progressOffset += progress; return output; }) .join(""); finalLayout = ` <mask id="rect-mask"> <rect x="25" y="0" width="${width - 50}" height="8" fill="white" rx="5" /> </mask> ${compactProgressBar} ${createLanguageTextNode({ x: 0, y: 25, langs: filteredLanguages, totalSize: 100, }).join("")} `; } else { finalLayout = flexLayout({ items: filteredLanguages.length ? filteredLanguages.map((language) => { return createTextNode({ id: language.name, label: language.name, value: language.text, percent: language.percent, // @ts-ignore progressBarColor: titleColor, // @ts-ignore progressBarBackgroundColor: textColor, hideProgress: hide_progress, }); }) : [ noCodingActivityNode({ // @ts-ignore color: textColor, text: i18n.t("wakatimecard.nocodingactivity"), }), ], gap: lheight, direction: "column", }).join(""); } const card = new Card({ customTitle: custom_title, defaultTitle: i18n.t("wakatimecard.title"), width: 495, height, border_radius, colors: { titleColor, textColor, iconColor, bgColor, borderColor, }, }); card.setHideBorder(hide_border); card.setHideTitle(hide_title); card.setCSS( ` ${cssStyles} .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } `, ); return card.render(` <svg x="0" y="0" width="100%"> ${finalLayout} </svg> `); }; export { renderWakatimeCard }; export default renderWakatimeCard;