Spaces:
Sleeping
Sleeping
Upload 34 files
Browse files- src/calculateRank.js +87 -0
- src/cards/gist.js +152 -0
- src/cards/index.js +4 -0
- src/cards/repo.js +193 -0
- src/cards/stats.js +602 -0
- src/cards/top-languages.js +965 -0
- src/cards/types.d.ts +67 -0
- src/cards/wakatime.js +481 -0
- src/common/Card.js +275 -0
- src/common/I18n.js +43 -0
- src/common/access.js +69 -0
- src/common/blacklist.js +10 -0
- src/common/cache.js +153 -0
- src/common/color.js +144 -0
- src/common/envs.js +15 -0
- src/common/error.js +84 -0
- src/common/fmt.js +90 -0
- src/common/html.js +19 -0
- src/common/http.js +21 -0
- src/common/icons.js +55 -0
- src/common/index.js +13 -0
- src/common/languageColors.json +651 -0
- src/common/log.js +14 -0
- src/common/ops.js +124 -0
- src/common/render.js +239 -0
- src/common/retryer.js +97 -0
- src/fetchers/gist.js +114 -0
- src/fetchers/repo.js +118 -0
- src/fetchers/stats.js +340 -0
- src/fetchers/top-languages.js +162 -0
- src/fetchers/types.d.ts +118 -0
- src/fetchers/wakatime.js +37 -0
- src/index.js +2 -0
- src/translations.js +1105 -0
src/calculateRank.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Calculates the exponential cdf.
|
| 3 |
+
*
|
| 4 |
+
* @param {number} x The value.
|
| 5 |
+
* @returns {number} The exponential cdf.
|
| 6 |
+
*/
|
| 7 |
+
function exponential_cdf(x) {
|
| 8 |
+
return 1 - 2 ** -x;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Calculates the log normal cdf.
|
| 13 |
+
*
|
| 14 |
+
* @param {number} x The value.
|
| 15 |
+
* @returns {number} The log normal cdf.
|
| 16 |
+
*/
|
| 17 |
+
function log_normal_cdf(x) {
|
| 18 |
+
// approximation
|
| 19 |
+
return x / (1 + x);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Calculates the users rank.
|
| 24 |
+
*
|
| 25 |
+
* @param {object} params Parameters on which the user's rank depends.
|
| 26 |
+
* @param {boolean} params.all_commits Whether `include_all_commits` was used.
|
| 27 |
+
* @param {number} params.commits Number of commits.
|
| 28 |
+
* @param {number} params.prs The number of pull requests.
|
| 29 |
+
* @param {number} params.issues The number of issues.
|
| 30 |
+
* @param {number} params.reviews The number of reviews.
|
| 31 |
+
* @param {number} params.repos Total number of repos.
|
| 32 |
+
* @param {number} params.stars The number of stars.
|
| 33 |
+
* @param {number} params.followers The number of followers.
|
| 34 |
+
* @returns {{ level: string, percentile: number }} The users rank.
|
| 35 |
+
*/
|
| 36 |
+
function calculateRank({
|
| 37 |
+
all_commits,
|
| 38 |
+
commits,
|
| 39 |
+
prs,
|
| 40 |
+
issues,
|
| 41 |
+
reviews,
|
| 42 |
+
// eslint-disable-next-line no-unused-vars
|
| 43 |
+
repos, // unused
|
| 44 |
+
stars,
|
| 45 |
+
followers,
|
| 46 |
+
}) {
|
| 47 |
+
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
|
| 48 |
+
COMMITS_WEIGHT = 2;
|
| 49 |
+
const PRS_MEDIAN = 50,
|
| 50 |
+
PRS_WEIGHT = 3;
|
| 51 |
+
const ISSUES_MEDIAN = 25,
|
| 52 |
+
ISSUES_WEIGHT = 1;
|
| 53 |
+
const REVIEWS_MEDIAN = 2,
|
| 54 |
+
REVIEWS_WEIGHT = 1;
|
| 55 |
+
const STARS_MEDIAN = 50,
|
| 56 |
+
STARS_WEIGHT = 4;
|
| 57 |
+
const FOLLOWERS_MEDIAN = 10,
|
| 58 |
+
FOLLOWERS_WEIGHT = 1;
|
| 59 |
+
|
| 60 |
+
const TOTAL_WEIGHT =
|
| 61 |
+
COMMITS_WEIGHT +
|
| 62 |
+
PRS_WEIGHT +
|
| 63 |
+
ISSUES_WEIGHT +
|
| 64 |
+
REVIEWS_WEIGHT +
|
| 65 |
+
STARS_WEIGHT +
|
| 66 |
+
FOLLOWERS_WEIGHT;
|
| 67 |
+
|
| 68 |
+
const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100];
|
| 69 |
+
const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"];
|
| 70 |
+
|
| 71 |
+
const rank =
|
| 72 |
+
1 -
|
| 73 |
+
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
|
| 74 |
+
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
|
| 75 |
+
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
|
| 76 |
+
REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) +
|
| 77 |
+
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
|
| 78 |
+
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
|
| 79 |
+
TOTAL_WEIGHT;
|
| 80 |
+
|
| 81 |
+
const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];
|
| 82 |
+
|
| 83 |
+
return { level, percentile: rank * 100 };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export { calculateRank };
|
| 87 |
+
export default calculateRank;
|
src/cards/gist.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
measureText,
|
| 5 |
+
flexLayout,
|
| 6 |
+
iconWithLabel,
|
| 7 |
+
createLanguageNode,
|
| 8 |
+
} from "../common/render.js";
|
| 9 |
+
import Card from "../common/Card.js";
|
| 10 |
+
import { getCardColors } from "../common/color.js";
|
| 11 |
+
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
| 12 |
+
import { encodeHTML } from "../common/html.js";
|
| 13 |
+
import { icons } from "../common/icons.js";
|
| 14 |
+
import { parseEmojis } from "../common/ops.js";
|
| 15 |
+
|
| 16 |
+
/** Import language colors.
|
| 17 |
+
*
|
| 18 |
+
* @description Here we use the workaround found in
|
| 19 |
+
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
|
| 20 |
+
* since vercel is using v16.14.0 which does not yet support json imports without the
|
| 21 |
+
* --experimental-json-modules flag.
|
| 22 |
+
*/
|
| 23 |
+
import { createRequire } from "module";
|
| 24 |
+
const require = createRequire(import.meta.url);
|
| 25 |
+
const languageColors = require("../common/languageColors.json"); // now works
|
| 26 |
+
|
| 27 |
+
const ICON_SIZE = 16;
|
| 28 |
+
const CARD_DEFAULT_WIDTH = 400;
|
| 29 |
+
const HEADER_MAX_LENGTH = 35;
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
|
| 33 |
+
* @typedef {import('../fetchers/types').GistData} GistData Gist data.
|
| 34 |
+
*/
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Render gist card.
|
| 38 |
+
*
|
| 39 |
+
* @param {GistData} gistData Gist data.
|
| 40 |
+
* @param {Partial<GistCardOptions>} options Gist card options.
|
| 41 |
+
* @returns {string} Gist card.
|
| 42 |
+
*/
|
| 43 |
+
const renderGistCard = (gistData, options = {}) => {
|
| 44 |
+
const { name, nameWithOwner, description, language, starsCount, forksCount } =
|
| 45 |
+
gistData;
|
| 46 |
+
const {
|
| 47 |
+
title_color,
|
| 48 |
+
icon_color,
|
| 49 |
+
text_color,
|
| 50 |
+
bg_color,
|
| 51 |
+
theme,
|
| 52 |
+
border_radius,
|
| 53 |
+
border_color,
|
| 54 |
+
show_owner = false,
|
| 55 |
+
hide_border = false,
|
| 56 |
+
} = options;
|
| 57 |
+
|
| 58 |
+
// returns theme based colors with proper overrides and defaults
|
| 59 |
+
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
| 60 |
+
getCardColors({
|
| 61 |
+
title_color,
|
| 62 |
+
icon_color,
|
| 63 |
+
text_color,
|
| 64 |
+
bg_color,
|
| 65 |
+
border_color,
|
| 66 |
+
theme,
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
const lineWidth = 59;
|
| 70 |
+
const linesLimit = 10;
|
| 71 |
+
const desc = parseEmojis(description || "No description provided");
|
| 72 |
+
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
|
| 73 |
+
const descriptionLines = multiLineDescription.length;
|
| 74 |
+
const descriptionSvg = multiLineDescription
|
| 75 |
+
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
| 76 |
+
.join("");
|
| 77 |
+
|
| 78 |
+
const lineHeight = descriptionLines > 3 ? 12 : 10;
|
| 79 |
+
const height =
|
| 80 |
+
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
|
| 81 |
+
|
| 82 |
+
const totalStars = kFormatter(starsCount);
|
| 83 |
+
const totalForks = kFormatter(forksCount);
|
| 84 |
+
const svgStars = iconWithLabel(
|
| 85 |
+
icons.star,
|
| 86 |
+
totalStars,
|
| 87 |
+
"starsCount",
|
| 88 |
+
ICON_SIZE,
|
| 89 |
+
);
|
| 90 |
+
const svgForks = iconWithLabel(
|
| 91 |
+
icons.fork,
|
| 92 |
+
totalForks,
|
| 93 |
+
"forksCount",
|
| 94 |
+
ICON_SIZE,
|
| 95 |
+
);
|
| 96 |
+
|
| 97 |
+
const languageName = language || "Unspecified";
|
| 98 |
+
// @ts-ignore
|
| 99 |
+
const languageColor = languageColors[languageName] || "#858585";
|
| 100 |
+
|
| 101 |
+
const svgLanguage = createLanguageNode(languageName, languageColor);
|
| 102 |
+
|
| 103 |
+
const starAndForkCount = flexLayout({
|
| 104 |
+
items: [svgLanguage, svgStars, svgForks],
|
| 105 |
+
sizes: [
|
| 106 |
+
measureText(languageName, 12),
|
| 107 |
+
ICON_SIZE + measureText(`${totalStars}`, 12),
|
| 108 |
+
ICON_SIZE + measureText(`${totalForks}`, 12),
|
| 109 |
+
],
|
| 110 |
+
gap: 25,
|
| 111 |
+
}).join("");
|
| 112 |
+
|
| 113 |
+
const header = show_owner ? nameWithOwner : name;
|
| 114 |
+
|
| 115 |
+
const card = new Card({
|
| 116 |
+
defaultTitle:
|
| 117 |
+
header.length > HEADER_MAX_LENGTH
|
| 118 |
+
? `${header.slice(0, HEADER_MAX_LENGTH)}...`
|
| 119 |
+
: header,
|
| 120 |
+
titlePrefixIcon: icons.gist,
|
| 121 |
+
width: CARD_DEFAULT_WIDTH,
|
| 122 |
+
height,
|
| 123 |
+
border_radius,
|
| 124 |
+
colors: {
|
| 125 |
+
titleColor,
|
| 126 |
+
textColor,
|
| 127 |
+
iconColor,
|
| 128 |
+
bgColor,
|
| 129 |
+
borderColor,
|
| 130 |
+
},
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
card.setCSS(`
|
| 134 |
+
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
| 135 |
+
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
| 136 |
+
.icon { fill: ${iconColor} }
|
| 137 |
+
`);
|
| 138 |
+
card.setHideBorder(hide_border);
|
| 139 |
+
|
| 140 |
+
return card.render(`
|
| 141 |
+
<text class="description" x="25" y="-5">
|
| 142 |
+
${descriptionSvg}
|
| 143 |
+
</text>
|
| 144 |
+
|
| 145 |
+
<g transform="translate(30, ${height - 75})">
|
| 146 |
+
${starAndForkCount}
|
| 147 |
+
</g>
|
| 148 |
+
`);
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
export { renderGistCard, HEADER_MAX_LENGTH };
|
| 152 |
+
export default renderGistCard;
|
src/cards/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { renderRepoCard } from "./repo.js";
|
| 2 |
+
export { renderStatsCard } from "./stats.js";
|
| 3 |
+
export { renderTopLanguages } from "./top-languages.js";
|
| 4 |
+
export { renderWakatimeCard } from "./wakatime.js";
|
src/cards/repo.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { Card } from "../common/Card.js";
|
| 4 |
+
import { getCardColors } from "../common/color.js";
|
| 5 |
+
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
| 6 |
+
import { encodeHTML } from "../common/html.js";
|
| 7 |
+
import { I18n } from "../common/I18n.js";
|
| 8 |
+
import { icons } from "../common/icons.js";
|
| 9 |
+
import { clampValue, parseEmojis } from "../common/ops.js";
|
| 10 |
+
import {
|
| 11 |
+
flexLayout,
|
| 12 |
+
measureText,
|
| 13 |
+
iconWithLabel,
|
| 14 |
+
createLanguageNode,
|
| 15 |
+
} from "../common/render.js";
|
| 16 |
+
import { repoCardLocales } from "../translations.js";
|
| 17 |
+
|
| 18 |
+
const ICON_SIZE = 16;
|
| 19 |
+
const DESCRIPTION_LINE_WIDTH = 59;
|
| 20 |
+
const DESCRIPTION_MAX_LINES = 3;
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Retrieves the repository description and wraps it to fit the card width.
|
| 24 |
+
*
|
| 25 |
+
* @param {string} label The repository description.
|
| 26 |
+
* @param {string} textColor The color of the text.
|
| 27 |
+
* @returns {string} Wrapped repo description SVG object.
|
| 28 |
+
*/
|
| 29 |
+
const getBadgeSVG = (label, textColor) => `
|
| 30 |
+
<g data-testid="badge" class="badge" transform="translate(320, -18)">
|
| 31 |
+
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
| 32 |
+
<text
|
| 33 |
+
x="23" y="-5"
|
| 34 |
+
alignment-baseline="central"
|
| 35 |
+
dominant-baseline="central"
|
| 36 |
+
text-anchor="middle"
|
| 37 |
+
fill="${textColor}"
|
| 38 |
+
>
|
| 39 |
+
${label}
|
| 40 |
+
</text>
|
| 41 |
+
</g>
|
| 42 |
+
`;
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
|
| 46 |
+
* @typedef {import("./types").RepoCardOptions} RepoCardOptions Repo card options.
|
| 47 |
+
*/
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Renders repository card details.
|
| 51 |
+
*
|
| 52 |
+
* @param {RepositoryData} repo Repository data.
|
| 53 |
+
* @param {Partial<RepoCardOptions>} options Card options.
|
| 54 |
+
* @returns {string} Repository card SVG object.
|
| 55 |
+
*/
|
| 56 |
+
const renderRepoCard = (repo, options = {}) => {
|
| 57 |
+
const {
|
| 58 |
+
name,
|
| 59 |
+
nameWithOwner,
|
| 60 |
+
description,
|
| 61 |
+
primaryLanguage,
|
| 62 |
+
isArchived,
|
| 63 |
+
isTemplate,
|
| 64 |
+
starCount,
|
| 65 |
+
forkCount,
|
| 66 |
+
} = repo;
|
| 67 |
+
const {
|
| 68 |
+
hide_border = false,
|
| 69 |
+
title_color,
|
| 70 |
+
icon_color,
|
| 71 |
+
text_color,
|
| 72 |
+
bg_color,
|
| 73 |
+
show_owner = false,
|
| 74 |
+
theme = "default_repocard",
|
| 75 |
+
border_radius,
|
| 76 |
+
border_color,
|
| 77 |
+
locale,
|
| 78 |
+
description_lines_count,
|
| 79 |
+
} = options;
|
| 80 |
+
|
| 81 |
+
const lineHeight = 10;
|
| 82 |
+
const header = show_owner ? nameWithOwner : name;
|
| 83 |
+
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
| 84 |
+
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
|
| 85 |
+
const descriptionMaxLines = description_lines_count
|
| 86 |
+
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
| 87 |
+
: DESCRIPTION_MAX_LINES;
|
| 88 |
+
|
| 89 |
+
const desc = parseEmojis(description || "No description provided");
|
| 90 |
+
const multiLineDescription = wrapTextMultiline(
|
| 91 |
+
desc,
|
| 92 |
+
DESCRIPTION_LINE_WIDTH,
|
| 93 |
+
descriptionMaxLines,
|
| 94 |
+
);
|
| 95 |
+
const descriptionLinesCount = description_lines_count
|
| 96 |
+
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
| 97 |
+
: multiLineDescription.length;
|
| 98 |
+
|
| 99 |
+
const descriptionSvg = multiLineDescription
|
| 100 |
+
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
| 101 |
+
.join("");
|
| 102 |
+
|
| 103 |
+
const height =
|
| 104 |
+
(descriptionLinesCount > 1 ? 120 : 110) +
|
| 105 |
+
descriptionLinesCount * lineHeight;
|
| 106 |
+
|
| 107 |
+
const i18n = new I18n({
|
| 108 |
+
locale,
|
| 109 |
+
translations: repoCardLocales,
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
// returns theme based colors with proper overrides and defaults
|
| 113 |
+
const colors = getCardColors({
|
| 114 |
+
title_color,
|
| 115 |
+
icon_color,
|
| 116 |
+
text_color,
|
| 117 |
+
bg_color,
|
| 118 |
+
border_color,
|
| 119 |
+
theme,
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
const svgLanguage = primaryLanguage
|
| 123 |
+
? createLanguageNode(langName, langColor)
|
| 124 |
+
: "";
|
| 125 |
+
|
| 126 |
+
const totalStars = kFormatter(starCount);
|
| 127 |
+
const totalForks = kFormatter(forkCount);
|
| 128 |
+
const svgStars = iconWithLabel(
|
| 129 |
+
icons.star,
|
| 130 |
+
totalStars,
|
| 131 |
+
"stargazers",
|
| 132 |
+
ICON_SIZE,
|
| 133 |
+
);
|
| 134 |
+
const svgForks = iconWithLabel(
|
| 135 |
+
icons.fork,
|
| 136 |
+
totalForks,
|
| 137 |
+
"forkcount",
|
| 138 |
+
ICON_SIZE,
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
const starAndForkCount = flexLayout({
|
| 142 |
+
items: [svgLanguage, svgStars, svgForks],
|
| 143 |
+
sizes: [
|
| 144 |
+
measureText(langName, 12),
|
| 145 |
+
ICON_SIZE + measureText(`${totalStars}`, 12),
|
| 146 |
+
ICON_SIZE + measureText(`${totalForks}`, 12),
|
| 147 |
+
],
|
| 148 |
+
gap: 25,
|
| 149 |
+
}).join("");
|
| 150 |
+
|
| 151 |
+
const card = new Card({
|
| 152 |
+
defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
|
| 153 |
+
titlePrefixIcon: icons.contribs,
|
| 154 |
+
width: 400,
|
| 155 |
+
height,
|
| 156 |
+
border_radius,
|
| 157 |
+
colors,
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
card.disableAnimations();
|
| 161 |
+
card.setHideBorder(hide_border);
|
| 162 |
+
card.setHideTitle(false);
|
| 163 |
+
card.setCSS(`
|
| 164 |
+
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
| 165 |
+
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
| 166 |
+
.icon { fill: ${colors.iconColor} }
|
| 167 |
+
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
| 168 |
+
.badge rect { opacity: 0.2 }
|
| 169 |
+
`);
|
| 170 |
+
|
| 171 |
+
return card.render(`
|
| 172 |
+
${
|
| 173 |
+
isTemplate
|
| 174 |
+
? // @ts-ignore
|
| 175 |
+
getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
|
| 176 |
+
: isArchived
|
| 177 |
+
? // @ts-ignore
|
| 178 |
+
getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
|
| 179 |
+
: ""
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
<text class="description" x="25" y="-5">
|
| 183 |
+
${descriptionSvg}
|
| 184 |
+
</text>
|
| 185 |
+
|
| 186 |
+
<g transform="translate(30, ${height - 75})">
|
| 187 |
+
${starAndForkCount}
|
| 188 |
+
</g>
|
| 189 |
+
`);
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
export { renderRepoCard };
|
| 193 |
+
export default renderRepoCard;
|
src/cards/stats.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { Card } from "../common/Card.js";
|
| 4 |
+
import { getCardColors } from "../common/color.js";
|
| 5 |
+
import { CustomError } from "../common/error.js";
|
| 6 |
+
import { kFormatter } from "../common/fmt.js";
|
| 7 |
+
import { I18n } from "../common/I18n.js";
|
| 8 |
+
import { icons, rankIcon } from "../common/icons.js";
|
| 9 |
+
import { clampValue } from "../common/ops.js";
|
| 10 |
+
import { flexLayout, measureText } from "../common/render.js";
|
| 11 |
+
import { statCardLocales, wakatimeCardLocales } from "../translations.js";
|
| 12 |
+
|
| 13 |
+
const CARD_MIN_WIDTH = 287;
|
| 14 |
+
const CARD_DEFAULT_WIDTH = 287;
|
| 15 |
+
const RANK_CARD_MIN_WIDTH = 420;
|
| 16 |
+
const RANK_CARD_DEFAULT_WIDTH = 450;
|
| 17 |
+
const RANK_ONLY_CARD_MIN_WIDTH = 290;
|
| 18 |
+
const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Long locales that need more space for text. Keep sorted alphabetically.
|
| 22 |
+
*
|
| 23 |
+
* @type {(keyof typeof wakatimeCardLocales["wakatimecard.title"])[]}
|
| 24 |
+
*/
|
| 25 |
+
const LONG_LOCALES = [
|
| 26 |
+
"az",
|
| 27 |
+
"bg",
|
| 28 |
+
"cs",
|
| 29 |
+
"de",
|
| 30 |
+
"el",
|
| 31 |
+
"es",
|
| 32 |
+
"fil",
|
| 33 |
+
"fi",
|
| 34 |
+
"fr",
|
| 35 |
+
"hu",
|
| 36 |
+
"id",
|
| 37 |
+
"ja",
|
| 38 |
+
"ml",
|
| 39 |
+
"my",
|
| 40 |
+
"nl",
|
| 41 |
+
"pl",
|
| 42 |
+
"pt-br",
|
| 43 |
+
"pt-pt",
|
| 44 |
+
"ru",
|
| 45 |
+
"sr",
|
| 46 |
+
"sr-latn",
|
| 47 |
+
"sw",
|
| 48 |
+
"ta",
|
| 49 |
+
"uk-ua",
|
| 50 |
+
"uz",
|
| 51 |
+
"zh-tw",
|
| 52 |
+
];
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Create a stats card text item.
|
| 56 |
+
*
|
| 57 |
+
* @param {object} params Object that contains the createTextNode parameters.
|
| 58 |
+
* @param {string} params.icon The icon to display.
|
| 59 |
+
* @param {string} params.label The label to display.
|
| 60 |
+
* @param {number} params.value The value to display.
|
| 61 |
+
* @param {string} params.id The id of the stat.
|
| 62 |
+
* @param {string=} params.unitSymbol The unit symbol of the stat.
|
| 63 |
+
* @param {number} params.index The index of the stat.
|
| 64 |
+
* @param {boolean} params.showIcons Whether to show icons.
|
| 65 |
+
* @param {number} params.shiftValuePos Number of pixels the value has to be shifted to the right.
|
| 66 |
+
* @param {boolean} params.bold Whether to bold the label.
|
| 67 |
+
* @param {string} params.numberFormat The format of numbers on card.
|
| 68 |
+
* @param {number=} params.numberPrecision The precision of numbers on card.
|
| 69 |
+
* @returns {string} The stats card text item SVG object.
|
| 70 |
+
*/
|
| 71 |
+
const createTextNode = ({
|
| 72 |
+
icon,
|
| 73 |
+
label,
|
| 74 |
+
value,
|
| 75 |
+
id,
|
| 76 |
+
unitSymbol,
|
| 77 |
+
index,
|
| 78 |
+
showIcons,
|
| 79 |
+
shiftValuePos,
|
| 80 |
+
bold,
|
| 81 |
+
numberFormat,
|
| 82 |
+
numberPrecision,
|
| 83 |
+
}) => {
|
| 84 |
+
const precision =
|
| 85 |
+
typeof numberPrecision === "number" && !isNaN(numberPrecision)
|
| 86 |
+
? clampValue(numberPrecision, 0, 2)
|
| 87 |
+
: undefined;
|
| 88 |
+
const kValue =
|
| 89 |
+
numberFormat.toLowerCase() === "long" || id === "prs_merged_percentage"
|
| 90 |
+
? value
|
| 91 |
+
: kFormatter(value, precision);
|
| 92 |
+
const staggerDelay = (index + 3) * 150;
|
| 93 |
+
|
| 94 |
+
const labelOffset = showIcons ? `x="25"` : "";
|
| 95 |
+
const iconSvg = showIcons
|
| 96 |
+
? `
|
| 97 |
+
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
| 98 |
+
${icon}
|
| 99 |
+
</svg>
|
| 100 |
+
`
|
| 101 |
+
: "";
|
| 102 |
+
return `
|
| 103 |
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
| 104 |
+
${iconSvg}
|
| 105 |
+
<text class="stat ${
|
| 106 |
+
bold ? " bold" : "not_bold"
|
| 107 |
+
}" ${labelOffset} y="12.5">${label}:</text>
|
| 108 |
+
<text
|
| 109 |
+
class="stat ${bold ? " bold" : "not_bold"}"
|
| 110 |
+
x="${(showIcons ? 140 : 120) + shiftValuePos}"
|
| 111 |
+
y="12.5"
|
| 112 |
+
data-testid="${id}"
|
| 113 |
+
>${kValue}${unitSymbol ? ` ${unitSymbol}` : ""}</text>
|
| 114 |
+
</g>
|
| 115 |
+
`;
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Calculates progress along the boundary of the circle, i.e. its circumference.
|
| 120 |
+
*
|
| 121 |
+
* @param {number} value The rank value to calculate progress for.
|
| 122 |
+
* @returns {number} Progress value.
|
| 123 |
+
*/
|
| 124 |
+
const calculateCircleProgress = (value) => {
|
| 125 |
+
const radius = 40;
|
| 126 |
+
const c = Math.PI * (radius * 2);
|
| 127 |
+
|
| 128 |
+
if (value < 0) {
|
| 129 |
+
value = 0;
|
| 130 |
+
}
|
| 131 |
+
if (value > 100) {
|
| 132 |
+
value = 100;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return ((100 - value) / 100) * c;
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Retrieves the animation to display progress along the circumference of circle
|
| 140 |
+
* from the beginning to the given value in a clockwise direction.
|
| 141 |
+
*
|
| 142 |
+
* @param {{progress: number}} progress The progress value to animate to.
|
| 143 |
+
* @returns {string} Progress animation css.
|
| 144 |
+
*/
|
| 145 |
+
const getProgressAnimation = ({ progress }) => {
|
| 146 |
+
return `
|
| 147 |
+
@keyframes rankAnimation {
|
| 148 |
+
from {
|
| 149 |
+
stroke-dashoffset: ${calculateCircleProgress(0)};
|
| 150 |
+
}
|
| 151 |
+
to {
|
| 152 |
+
stroke-dashoffset: ${calculateCircleProgress(progress)};
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
`;
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Retrieves CSS styles for a card.
|
| 160 |
+
*
|
| 161 |
+
* @param {Object} colors The colors to use for the card.
|
| 162 |
+
* @param {string} colors.titleColor The title color.
|
| 163 |
+
* @param {string} colors.textColor The text color.
|
| 164 |
+
* @param {string} colors.iconColor The icon color.
|
| 165 |
+
* @param {string} colors.ringColor The ring color.
|
| 166 |
+
* @param {boolean} colors.show_icons Whether to show icons.
|
| 167 |
+
* @param {number} colors.progress The progress value to animate to.
|
| 168 |
+
* @returns {string} Card CSS styles.
|
| 169 |
+
*/
|
| 170 |
+
const getStyles = ({
|
| 171 |
+
// eslint-disable-next-line no-unused-vars
|
| 172 |
+
titleColor,
|
| 173 |
+
textColor,
|
| 174 |
+
iconColor,
|
| 175 |
+
ringColor,
|
| 176 |
+
show_icons,
|
| 177 |
+
progress,
|
| 178 |
+
}) => {
|
| 179 |
+
return `
|
| 180 |
+
.stat {
|
| 181 |
+
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
| 182 |
+
}
|
| 183 |
+
@supports(-moz-appearance: auto) {
|
| 184 |
+
/* Selector detects Firefox */
|
| 185 |
+
.stat { font-size:12px; }
|
| 186 |
+
}
|
| 187 |
+
.stagger {
|
| 188 |
+
opacity: 0;
|
| 189 |
+
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
| 190 |
+
}
|
| 191 |
+
.rank-text {
|
| 192 |
+
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
|
| 193 |
+
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
| 194 |
+
}
|
| 195 |
+
.rank-percentile-header {
|
| 196 |
+
font-size: 14px;
|
| 197 |
+
}
|
| 198 |
+
.rank-percentile-text {
|
| 199 |
+
font-size: 16px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.not_bold { font-weight: 400 }
|
| 203 |
+
.bold { font-weight: 700 }
|
| 204 |
+
.icon {
|
| 205 |
+
fill: ${iconColor};
|
| 206 |
+
display: ${show_icons ? "block" : "none"};
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.rank-circle-rim {
|
| 210 |
+
stroke: ${ringColor};
|
| 211 |
+
fill: none;
|
| 212 |
+
stroke-width: 6;
|
| 213 |
+
opacity: 0.2;
|
| 214 |
+
}
|
| 215 |
+
.rank-circle {
|
| 216 |
+
stroke: ${ringColor};
|
| 217 |
+
stroke-dasharray: 250;
|
| 218 |
+
fill: none;
|
| 219 |
+
stroke-width: 6;
|
| 220 |
+
stroke-linecap: round;
|
| 221 |
+
opacity: 0.8;
|
| 222 |
+
transform-origin: -10px 8px;
|
| 223 |
+
transform: rotate(-90deg);
|
| 224 |
+
animation: rankAnimation 1s forwards ease-in-out;
|
| 225 |
+
}
|
| 226 |
+
${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
|
| 227 |
+
`;
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
/**
|
| 231 |
+
* Return the label for commits according to the selected options
|
| 232 |
+
*
|
| 233 |
+
* @param {boolean} include_all_commits Option to include all years
|
| 234 |
+
* @param {number|undefined} commits_year Option to include only selected year
|
| 235 |
+
* @param {I18n} i18n The I18n instance.
|
| 236 |
+
* @returns {string} The label corresponding to the options.
|
| 237 |
+
*/
|
| 238 |
+
const getTotalCommitsYearLabel = (include_all_commits, commits_year, i18n) =>
|
| 239 |
+
include_all_commits
|
| 240 |
+
? ""
|
| 241 |
+
: commits_year
|
| 242 |
+
? ` (${commits_year})`
|
| 243 |
+
: ` (${i18n.t("wakatimecard.lastyear")})`;
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* @typedef {import('../fetchers/types').StatsData} StatsData
|
| 247 |
+
* @typedef {import('./types').StatCardOptions} StatCardOptions
|
| 248 |
+
*/
|
| 249 |
+
|
| 250 |
+
/**
|
| 251 |
+
* Renders the stats card.
|
| 252 |
+
*
|
| 253 |
+
* @param {StatsData} stats The stats data.
|
| 254 |
+
* @param {Partial<StatCardOptions>} options The card options.
|
| 255 |
+
* @returns {string} The stats card SVG object.
|
| 256 |
+
*/
|
| 257 |
+
const renderStatsCard = (stats, options = {}) => {
|
| 258 |
+
const {
|
| 259 |
+
name,
|
| 260 |
+
totalStars,
|
| 261 |
+
totalCommits,
|
| 262 |
+
totalIssues,
|
| 263 |
+
totalPRs,
|
| 264 |
+
totalPRsMerged,
|
| 265 |
+
mergedPRsPercentage,
|
| 266 |
+
totalReviews,
|
| 267 |
+
totalDiscussionsStarted,
|
| 268 |
+
totalDiscussionsAnswered,
|
| 269 |
+
contributedTo,
|
| 270 |
+
rank,
|
| 271 |
+
} = stats;
|
| 272 |
+
const {
|
| 273 |
+
hide = [],
|
| 274 |
+
show_icons = false,
|
| 275 |
+
hide_title = false,
|
| 276 |
+
hide_border = false,
|
| 277 |
+
card_width,
|
| 278 |
+
hide_rank = false,
|
| 279 |
+
include_all_commits = false,
|
| 280 |
+
commits_year,
|
| 281 |
+
line_height = 25,
|
| 282 |
+
title_color,
|
| 283 |
+
ring_color,
|
| 284 |
+
icon_color,
|
| 285 |
+
text_color,
|
| 286 |
+
text_bold = true,
|
| 287 |
+
bg_color,
|
| 288 |
+
theme = "default",
|
| 289 |
+
custom_title,
|
| 290 |
+
border_radius,
|
| 291 |
+
border_color,
|
| 292 |
+
number_format = "short",
|
| 293 |
+
number_precision,
|
| 294 |
+
locale,
|
| 295 |
+
disable_animations = false,
|
| 296 |
+
rank_icon = "default",
|
| 297 |
+
show = [],
|
| 298 |
+
} = options;
|
| 299 |
+
|
| 300 |
+
const lheight = parseInt(String(line_height), 10);
|
| 301 |
+
|
| 302 |
+
// returns theme based colors with proper overrides and defaults
|
| 303 |
+
const { titleColor, iconColor, textColor, bgColor, borderColor, ringColor } =
|
| 304 |
+
getCardColors({
|
| 305 |
+
title_color,
|
| 306 |
+
text_color,
|
| 307 |
+
icon_color,
|
| 308 |
+
bg_color,
|
| 309 |
+
border_color,
|
| 310 |
+
ring_color,
|
| 311 |
+
theme,
|
| 312 |
+
});
|
| 313 |
+
|
| 314 |
+
const apostrophe = /s$/i.test(name.trim()) ? "" : "s";
|
| 315 |
+
const i18n = new I18n({
|
| 316 |
+
locale,
|
| 317 |
+
translations: {
|
| 318 |
+
...statCardLocales({ name, apostrophe }),
|
| 319 |
+
...wakatimeCardLocales,
|
| 320 |
+
},
|
| 321 |
+
});
|
| 322 |
+
|
| 323 |
+
// Meta data for creating text nodes with createTextNode function
|
| 324 |
+
const STATS = {};
|
| 325 |
+
|
| 326 |
+
STATS.stars = {
|
| 327 |
+
icon: icons.star,
|
| 328 |
+
label: i18n.t("statcard.totalstars"),
|
| 329 |
+
value: totalStars,
|
| 330 |
+
id: "stars",
|
| 331 |
+
};
|
| 332 |
+
STATS.commits = {
|
| 333 |
+
icon: icons.commits,
|
| 334 |
+
label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel(
|
| 335 |
+
include_all_commits,
|
| 336 |
+
commits_year,
|
| 337 |
+
i18n,
|
| 338 |
+
)}`,
|
| 339 |
+
value: totalCommits,
|
| 340 |
+
id: "commits",
|
| 341 |
+
};
|
| 342 |
+
STATS.prs = {
|
| 343 |
+
icon: icons.prs,
|
| 344 |
+
label: i18n.t("statcard.prs"),
|
| 345 |
+
value: totalPRs,
|
| 346 |
+
id: "prs",
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
if (show.includes("prs_merged")) {
|
| 350 |
+
STATS.prs_merged = {
|
| 351 |
+
icon: icons.prs_merged,
|
| 352 |
+
label: i18n.t("statcard.prs-merged"),
|
| 353 |
+
value: totalPRsMerged,
|
| 354 |
+
id: "prs_merged",
|
| 355 |
+
};
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
if (show.includes("prs_merged_percentage")) {
|
| 359 |
+
STATS.prs_merged_percentage = {
|
| 360 |
+
icon: icons.prs_merged_percentage,
|
| 361 |
+
label: i18n.t("statcard.prs-merged-percentage"),
|
| 362 |
+
value: mergedPRsPercentage.toFixed(
|
| 363 |
+
typeof number_precision === "number" && !isNaN(number_precision)
|
| 364 |
+
? clampValue(number_precision, 0, 2)
|
| 365 |
+
: 2,
|
| 366 |
+
),
|
| 367 |
+
id: "prs_merged_percentage",
|
| 368 |
+
unitSymbol: "%",
|
| 369 |
+
};
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
if (show.includes("reviews")) {
|
| 373 |
+
STATS.reviews = {
|
| 374 |
+
icon: icons.reviews,
|
| 375 |
+
label: i18n.t("statcard.reviews"),
|
| 376 |
+
value: totalReviews,
|
| 377 |
+
id: "reviews",
|
| 378 |
+
};
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
STATS.issues = {
|
| 382 |
+
icon: icons.issues,
|
| 383 |
+
label: i18n.t("statcard.issues"),
|
| 384 |
+
value: totalIssues,
|
| 385 |
+
id: "issues",
|
| 386 |
+
};
|
| 387 |
+
|
| 388 |
+
if (show.includes("discussions_started")) {
|
| 389 |
+
STATS.discussions_started = {
|
| 390 |
+
icon: icons.discussions_started,
|
| 391 |
+
label: i18n.t("statcard.discussions-started"),
|
| 392 |
+
value: totalDiscussionsStarted,
|
| 393 |
+
id: "discussions_started",
|
| 394 |
+
};
|
| 395 |
+
}
|
| 396 |
+
if (show.includes("discussions_answered")) {
|
| 397 |
+
STATS.discussions_answered = {
|
| 398 |
+
icon: icons.discussions_answered,
|
| 399 |
+
label: i18n.t("statcard.discussions-answered"),
|
| 400 |
+
value: totalDiscussionsAnswered,
|
| 401 |
+
id: "discussions_answered",
|
| 402 |
+
};
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
STATS.contribs = {
|
| 406 |
+
icon: icons.contribs,
|
| 407 |
+
label: i18n.t("statcard.contribs"),
|
| 408 |
+
value: contributedTo,
|
| 409 |
+
id: "contribs",
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
// @ts-ignore
|
| 413 |
+
const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
|
| 414 |
+
|
| 415 |
+
// filter out hidden stats defined by user & create the text nodes
|
| 416 |
+
const statItems = Object.keys(STATS)
|
| 417 |
+
.filter((key) => !hide.includes(key))
|
| 418 |
+
.map((key, index) => {
|
| 419 |
+
// @ts-ignore
|
| 420 |
+
const stats = STATS[key];
|
| 421 |
+
|
| 422 |
+
// create the text nodes, and pass index so that we can calculate the line spacing
|
| 423 |
+
return createTextNode({
|
| 424 |
+
icon: stats.icon,
|
| 425 |
+
label: stats.label,
|
| 426 |
+
value: stats.value,
|
| 427 |
+
id: stats.id,
|
| 428 |
+
unitSymbol: stats.unitSymbol,
|
| 429 |
+
index,
|
| 430 |
+
showIcons: show_icons,
|
| 431 |
+
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
|
| 432 |
+
bold: text_bold,
|
| 433 |
+
numberFormat: number_format,
|
| 434 |
+
numberPrecision: number_precision,
|
| 435 |
+
});
|
| 436 |
+
});
|
| 437 |
+
|
| 438 |
+
if (statItems.length === 0 && hide_rank) {
|
| 439 |
+
throw new CustomError(
|
| 440 |
+
"Could not render stats card.",
|
| 441 |
+
"Either stats or rank are required.",
|
| 442 |
+
);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
// Calculate the card height depending on how many items there are
|
| 446 |
+
// but if rank circle is visible clamp the minimum height to `150`
|
| 447 |
+
let height = Math.max(
|
| 448 |
+
45 + (statItems.length + 1) * lheight,
|
| 449 |
+
hide_rank ? 0 : statItems.length ? 150 : 180,
|
| 450 |
+
);
|
| 451 |
+
|
| 452 |
+
// the lower the user's percentile the better
|
| 453 |
+
const progress = 100 - rank.percentile;
|
| 454 |
+
const cssStyles = getStyles({
|
| 455 |
+
titleColor,
|
| 456 |
+
ringColor,
|
| 457 |
+
textColor,
|
| 458 |
+
iconColor,
|
| 459 |
+
show_icons,
|
| 460 |
+
progress,
|
| 461 |
+
});
|
| 462 |
+
|
| 463 |
+
const calculateTextWidth = () => {
|
| 464 |
+
return measureText(
|
| 465 |
+
custom_title
|
| 466 |
+
? custom_title
|
| 467 |
+
: statItems.length
|
| 468 |
+
? i18n.t("statcard.title")
|
| 469 |
+
: i18n.t("statcard.ranktitle"),
|
| 470 |
+
);
|
| 471 |
+
};
|
| 472 |
+
|
| 473 |
+
/*
|
| 474 |
+
When hide_rank=true, the minimum card width is 270 px + the title length and padding.
|
| 475 |
+
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
| 476 |
+
Numbers are picked by looking at existing dimensions on production.
|
| 477 |
+
*/
|
| 478 |
+
const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
|
| 479 |
+
const minCardWidth =
|
| 480 |
+
(hide_rank
|
| 481 |
+
? clampValue(
|
| 482 |
+
50 /* padding */ + calculateTextWidth() * 2,
|
| 483 |
+
CARD_MIN_WIDTH,
|
| 484 |
+
Infinity,
|
| 485 |
+
)
|
| 486 |
+
: statItems.length
|
| 487 |
+
? RANK_CARD_MIN_WIDTH
|
| 488 |
+
: RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
|
| 489 |
+
const defaultCardWidth =
|
| 490 |
+
(hide_rank
|
| 491 |
+
? CARD_DEFAULT_WIDTH
|
| 492 |
+
: statItems.length
|
| 493 |
+
? RANK_CARD_DEFAULT_WIDTH
|
| 494 |
+
: RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
|
| 495 |
+
let width = card_width
|
| 496 |
+
? isNaN(card_width)
|
| 497 |
+
? defaultCardWidth
|
| 498 |
+
: card_width
|
| 499 |
+
: defaultCardWidth;
|
| 500 |
+
if (width < minCardWidth) {
|
| 501 |
+
width = minCardWidth;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
const card = new Card({
|
| 505 |
+
customTitle: custom_title,
|
| 506 |
+
defaultTitle: statItems.length
|
| 507 |
+
? i18n.t("statcard.title")
|
| 508 |
+
: i18n.t("statcard.ranktitle"),
|
| 509 |
+
width,
|
| 510 |
+
height,
|
| 511 |
+
border_radius,
|
| 512 |
+
colors: {
|
| 513 |
+
titleColor,
|
| 514 |
+
textColor,
|
| 515 |
+
iconColor,
|
| 516 |
+
bgColor,
|
| 517 |
+
borderColor,
|
| 518 |
+
},
|
| 519 |
+
});
|
| 520 |
+
|
| 521 |
+
card.setHideBorder(hide_border);
|
| 522 |
+
card.setHideTitle(hide_title);
|
| 523 |
+
card.setCSS(cssStyles);
|
| 524 |
+
|
| 525 |
+
if (disable_animations) {
|
| 526 |
+
card.disableAnimations();
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
/**
|
| 530 |
+
* Calculates the right rank circle translation values such that the rank circle
|
| 531 |
+
* keeps respecting the following padding:
|
| 532 |
+
*
|
| 533 |
+
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
| 534 |
+
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
| 535 |
+
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
| 536 |
+
*
|
| 537 |
+
* @returns {number} - Rank circle translation value.
|
| 538 |
+
*/
|
| 539 |
+
const calculateRankXTranslation = () => {
|
| 540 |
+
if (statItems.length) {
|
| 541 |
+
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
| 542 |
+
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
| 543 |
+
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
| 544 |
+
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
| 545 |
+
} else {
|
| 546 |
+
return minXTranslation + (width - minCardWidth) / 2;
|
| 547 |
+
}
|
| 548 |
+
} else {
|
| 549 |
+
return width / 2 + 20 - 10;
|
| 550 |
+
}
|
| 551 |
+
};
|
| 552 |
+
|
| 553 |
+
// Conditionally rendered elements
|
| 554 |
+
const rankCircle = hide_rank
|
| 555 |
+
? ""
|
| 556 |
+
: `<g data-testid="rank-circle"
|
| 557 |
+
transform="translate(${calculateRankXTranslation()}, ${
|
| 558 |
+
height / 2 - 50
|
| 559 |
+
})">
|
| 560 |
+
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
| 561 |
+
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
| 562 |
+
<g class="rank-text">
|
| 563 |
+
${rankIcon(rank_icon, rank?.level, rank?.percentile)}
|
| 564 |
+
</g>
|
| 565 |
+
</g>`;
|
| 566 |
+
|
| 567 |
+
// Accessibility Labels
|
| 568 |
+
const labels = Object.keys(STATS)
|
| 569 |
+
.filter((key) => !hide.includes(key))
|
| 570 |
+
.map((key) => {
|
| 571 |
+
// @ts-ignore
|
| 572 |
+
const stats = STATS[key];
|
| 573 |
+
if (key === "commits") {
|
| 574 |
+
return `${i18n.t("statcard.commits")} ${getTotalCommitsYearLabel(
|
| 575 |
+
include_all_commits,
|
| 576 |
+
commits_year,
|
| 577 |
+
i18n,
|
| 578 |
+
)} : ${stats.value}`;
|
| 579 |
+
}
|
| 580 |
+
return `${stats.label}: ${stats.value}`;
|
| 581 |
+
})
|
| 582 |
+
.join(", ");
|
| 583 |
+
|
| 584 |
+
card.setAccessibilityLabel({
|
| 585 |
+
title: `${card.title}, Rank: ${rank.level}`,
|
| 586 |
+
desc: labels,
|
| 587 |
+
});
|
| 588 |
+
|
| 589 |
+
return card.render(`
|
| 590 |
+
${rankCircle}
|
| 591 |
+
<svg x="0" y="0">
|
| 592 |
+
${flexLayout({
|
| 593 |
+
items: statItems,
|
| 594 |
+
gap: lheight,
|
| 595 |
+
direction: "column",
|
| 596 |
+
}).join("")}
|
| 597 |
+
</svg>
|
| 598 |
+
`);
|
| 599 |
+
};
|
| 600 |
+
|
| 601 |
+
export { renderStatsCard };
|
| 602 |
+
export default renderStatsCard;
|
src/cards/top-languages.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { Card } from "../common/Card.js";
|
| 4 |
+
import { getCardColors } from "../common/color.js";
|
| 5 |
+
import { formatBytes } from "../common/fmt.js";
|
| 6 |
+
import { I18n } from "../common/I18n.js";
|
| 7 |
+
import { chunkArray, clampValue, lowercaseTrim } from "../common/ops.js";
|
| 8 |
+
import {
|
| 9 |
+
createProgressNode,
|
| 10 |
+
flexLayout,
|
| 11 |
+
measureText,
|
| 12 |
+
} from "../common/render.js";
|
| 13 |
+
import { langCardLocales } from "../translations.js";
|
| 14 |
+
|
| 15 |
+
const DEFAULT_CARD_WIDTH = 300;
|
| 16 |
+
const MIN_CARD_WIDTH = 280;
|
| 17 |
+
const DEFAULT_LANG_COLOR = "#858585";
|
| 18 |
+
const CARD_PADDING = 25;
|
| 19 |
+
const COMPACT_LAYOUT_BASE_HEIGHT = 90;
|
| 20 |
+
const MAXIMUM_LANGS_COUNT = 20;
|
| 21 |
+
|
| 22 |
+
const NORMAL_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
| 23 |
+
const COMPACT_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
| 24 |
+
const DONUT_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
| 25 |
+
const PIE_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
| 26 |
+
const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* @typedef {import("../fetchers/types").Lang} Lang
|
| 30 |
+
*/
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Retrieves the programming language whose name is the longest.
|
| 34 |
+
*
|
| 35 |
+
* @param {Lang[]} arr Array of programming languages.
|
| 36 |
+
* @returns {{ name: string, size: number, color: string }} Longest programming language object.
|
| 37 |
+
*/
|
| 38 |
+
const getLongestLang = (arr) =>
|
| 39 |
+
arr.reduce(
|
| 40 |
+
(savedLang, lang) =>
|
| 41 |
+
lang.name.length > savedLang.name.length ? lang : savedLang,
|
| 42 |
+
{ name: "", size: 0, color: "" },
|
| 43 |
+
);
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Convert degrees to radians.
|
| 47 |
+
*
|
| 48 |
+
* @param {number} angleInDegrees Angle in degrees.
|
| 49 |
+
* @returns {number} Angle in radians.
|
| 50 |
+
*/
|
| 51 |
+
const degreesToRadians = (angleInDegrees) => angleInDegrees * (Math.PI / 180.0);
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Convert radians to degrees.
|
| 55 |
+
*
|
| 56 |
+
* @param {number} angleInRadians Angle in radians.
|
| 57 |
+
* @returns {number} Angle in degrees.
|
| 58 |
+
*/
|
| 59 |
+
const radiansToDegrees = (angleInRadians) => angleInRadians / (Math.PI / 180.0);
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Convert polar coordinates to cartesian coordinates.
|
| 63 |
+
*
|
| 64 |
+
* @param {number} centerX Center x coordinate.
|
| 65 |
+
* @param {number} centerY Center y coordinate.
|
| 66 |
+
* @param {number} radius Radius of the circle.
|
| 67 |
+
* @param {number} angleInDegrees Angle in degrees.
|
| 68 |
+
* @returns {{x: number, y: number}} Cartesian coordinates.
|
| 69 |
+
*/
|
| 70 |
+
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
|
| 71 |
+
const rads = degreesToRadians(angleInDegrees);
|
| 72 |
+
return {
|
| 73 |
+
x: centerX + radius * Math.cos(rads),
|
| 74 |
+
y: centerY + radius * Math.sin(rads),
|
| 75 |
+
};
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Convert cartesian coordinates to polar coordinates.
|
| 80 |
+
*
|
| 81 |
+
* @param {number} centerX Center x coordinate.
|
| 82 |
+
* @param {number} centerY Center y coordinate.
|
| 83 |
+
* @param {number} x Point x coordinate.
|
| 84 |
+
* @param {number} y Point y coordinate.
|
| 85 |
+
* @returns {{radius: number, angleInDegrees: number}} Polar coordinates.
|
| 86 |
+
*/
|
| 87 |
+
const cartesianToPolar = (centerX, centerY, x, y) => {
|
| 88 |
+
const radius = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
| 89 |
+
let angleInDegrees = radiansToDegrees(Math.atan2(y - centerY, x - centerX));
|
| 90 |
+
if (angleInDegrees < 0) {
|
| 91 |
+
angleInDegrees += 360;
|
| 92 |
+
}
|
| 93 |
+
return { radius, angleInDegrees };
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Calculates length of circle.
|
| 98 |
+
*
|
| 99 |
+
* @param {number} radius Radius of the circle.
|
| 100 |
+
* @returns {number} The length of the circle.
|
| 101 |
+
*/
|
| 102 |
+
const getCircleLength = (radius) => {
|
| 103 |
+
return 2 * Math.PI * radius;
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Calculates height for the compact layout.
|
| 108 |
+
*
|
| 109 |
+
* @param {number} totalLangs Total number of languages.
|
| 110 |
+
* @returns {number} Card height.
|
| 111 |
+
*/
|
| 112 |
+
const calculateCompactLayoutHeight = (totalLangs) => {
|
| 113 |
+
return COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25;
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Calculates height for the normal layout.
|
| 118 |
+
*
|
| 119 |
+
* @param {number} totalLangs Total number of languages.
|
| 120 |
+
* @returns {number} Card height.
|
| 121 |
+
*/
|
| 122 |
+
const calculateNormalLayoutHeight = (totalLangs) => {
|
| 123 |
+
return 45 + (totalLangs + 1) * 40;
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* Calculates height for the donut layout.
|
| 128 |
+
*
|
| 129 |
+
* @param {number} totalLangs Total number of languages.
|
| 130 |
+
* @returns {number} Card height.
|
| 131 |
+
*/
|
| 132 |
+
const calculateDonutLayoutHeight = (totalLangs) => {
|
| 133 |
+
return 215 + Math.max(totalLangs - 5, 0) * 32;
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* Calculates height for the donut vertical layout.
|
| 138 |
+
*
|
| 139 |
+
* @param {number} totalLangs Total number of languages.
|
| 140 |
+
* @returns {number} Card height.
|
| 141 |
+
*/
|
| 142 |
+
const calculateDonutVerticalLayoutHeight = (totalLangs) => {
|
| 143 |
+
return 300 + Math.round(totalLangs / 2) * 25;
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
/**
|
| 147 |
+
* Calculates height for the pie layout.
|
| 148 |
+
*
|
| 149 |
+
* @param {number} totalLangs Total number of languages.
|
| 150 |
+
* @returns {number} Card height.
|
| 151 |
+
*/
|
| 152 |
+
const calculatePieLayoutHeight = (totalLangs) => {
|
| 153 |
+
return 300 + Math.round(totalLangs / 2) * 25;
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
/**
|
| 157 |
+
* Calculates the center translation needed to keep the donut chart centred.
|
| 158 |
+
* @param {number} totalLangs Total number of languages.
|
| 159 |
+
* @returns {number} Donut center translation.
|
| 160 |
+
*/
|
| 161 |
+
const donutCenterTranslation = (totalLangs) => {
|
| 162 |
+
return -45 + Math.max(totalLangs - 5, 0) * 16;
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* Trim top languages to lang_count while also hiding certain languages.
|
| 167 |
+
*
|
| 168 |
+
* @param {Record<string, Lang>} topLangs Top languages.
|
| 169 |
+
* @param {number} langs_count Number of languages to show.
|
| 170 |
+
* @param {string[]=} hide Languages to hide.
|
| 171 |
+
* @returns {{ langs: Lang[], totalLanguageSize: number }} Trimmed top languages and total size.
|
| 172 |
+
*/
|
| 173 |
+
const trimTopLanguages = (topLangs, langs_count, hide) => {
|
| 174 |
+
let langs = Object.values(topLangs);
|
| 175 |
+
let langsToHide = {};
|
| 176 |
+
let langsCount = clampValue(langs_count, 1, MAXIMUM_LANGS_COUNT);
|
| 177 |
+
|
| 178 |
+
// populate langsToHide map for quick lookup
|
| 179 |
+
// while filtering out
|
| 180 |
+
if (hide) {
|
| 181 |
+
hide.forEach((langName) => {
|
| 182 |
+
// @ts-ignore
|
| 183 |
+
langsToHide[lowercaseTrim(langName)] = true;
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// filter out languages to be hidden
|
| 188 |
+
langs = langs
|
| 189 |
+
.sort((a, b) => b.size - a.size)
|
| 190 |
+
.filter((lang) => {
|
| 191 |
+
// @ts-ignore
|
| 192 |
+
return !langsToHide[lowercaseTrim(lang.name)];
|
| 193 |
+
})
|
| 194 |
+
.slice(0, langsCount);
|
| 195 |
+
|
| 196 |
+
const totalLanguageSize = langs.reduce((acc, curr) => acc + curr.size, 0);
|
| 197 |
+
|
| 198 |
+
return { langs, totalLanguageSize };
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Get display value corresponding to the format.
|
| 203 |
+
*
|
| 204 |
+
* @param {number} size Bytes size.
|
| 205 |
+
* @param {number} percentages Percentage value.
|
| 206 |
+
* @param {string} format Format of the stats.
|
| 207 |
+
* @returns {string} Display value.
|
| 208 |
+
*/
|
| 209 |
+
const getDisplayValue = (size, percentages, format) => {
|
| 210 |
+
return format === "bytes" ? formatBytes(size) : `${percentages.toFixed(2)}%`;
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* Create progress bar text item for a programming language.
|
| 215 |
+
*
|
| 216 |
+
* @param {object} props Function properties.
|
| 217 |
+
* @param {number} props.width The card width
|
| 218 |
+
* @param {string} props.color Color of the programming language.
|
| 219 |
+
* @param {string} props.name Name of the programming language.
|
| 220 |
+
* @param {number} props.size Size of the programming language.
|
| 221 |
+
* @param {number} props.totalSize Total size of all languages.
|
| 222 |
+
* @param {string} props.statsFormat Stats format.
|
| 223 |
+
* @param {number} props.index Index of the programming language.
|
| 224 |
+
* @returns {string} Programming language SVG node.
|
| 225 |
+
*/
|
| 226 |
+
const createProgressTextNode = ({
|
| 227 |
+
width,
|
| 228 |
+
color,
|
| 229 |
+
name,
|
| 230 |
+
size,
|
| 231 |
+
totalSize,
|
| 232 |
+
statsFormat,
|
| 233 |
+
index,
|
| 234 |
+
}) => {
|
| 235 |
+
const staggerDelay = (index + 3) * 150;
|
| 236 |
+
const paddingRight = 95;
|
| 237 |
+
const progressTextX = width - paddingRight + 10;
|
| 238 |
+
const progressWidth = width - paddingRight;
|
| 239 |
+
|
| 240 |
+
const progress = (size / totalSize) * 100;
|
| 241 |
+
const displayValue = getDisplayValue(size, progress, statsFormat);
|
| 242 |
+
|
| 243 |
+
return `
|
| 244 |
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
| 245 |
+
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
| 246 |
+
<text x="${progressTextX}" y="34" class="lang-name">${displayValue}</text>
|
| 247 |
+
${createProgressNode({
|
| 248 |
+
x: 0,
|
| 249 |
+
y: 25,
|
| 250 |
+
color,
|
| 251 |
+
width: progressWidth,
|
| 252 |
+
progress,
|
| 253 |
+
progressBarBackgroundColor: "#ddd",
|
| 254 |
+
delay: staggerDelay + 300,
|
| 255 |
+
})}
|
| 256 |
+
</g>
|
| 257 |
+
`;
|
| 258 |
+
};
|
| 259 |
+
|
| 260 |
+
/**
|
| 261 |
+
* Creates compact text item for a programming language.
|
| 262 |
+
*
|
| 263 |
+
* @param {object} props Function properties.
|
| 264 |
+
* @param {Lang} props.lang Programming language object.
|
| 265 |
+
* @param {number} props.totalSize Total size of all languages.
|
| 266 |
+
* @param {boolean=} props.hideProgress Whether to hide percentage.
|
| 267 |
+
* @param {string=} props.statsFormat Stats format
|
| 268 |
+
* @param {number} props.index Index of the programming language.
|
| 269 |
+
* @returns {string} Compact layout programming language SVG node.
|
| 270 |
+
*/
|
| 271 |
+
const createCompactLangNode = ({
|
| 272 |
+
lang,
|
| 273 |
+
totalSize,
|
| 274 |
+
hideProgress,
|
| 275 |
+
statsFormat = "percentages",
|
| 276 |
+
index,
|
| 277 |
+
}) => {
|
| 278 |
+
const percentages = (lang.size / totalSize) * 100;
|
| 279 |
+
const displayValue = getDisplayValue(lang.size, percentages, statsFormat);
|
| 280 |
+
|
| 281 |
+
const staggerDelay = (index + 3) * 150;
|
| 282 |
+
const color = lang.color || "#858585";
|
| 283 |
+
|
| 284 |
+
return `
|
| 285 |
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
| 286 |
+
<circle cx="5" cy="6" r="5" fill="${color}" />
|
| 287 |
+
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
| 288 |
+
${lang.name} ${hideProgress ? "" : displayValue}
|
| 289 |
+
</text>
|
| 290 |
+
</g>
|
| 291 |
+
`;
|
| 292 |
+
};
|
| 293 |
+
|
| 294 |
+
/**
|
| 295 |
+
* Create compact languages text items for all programming languages.
|
| 296 |
+
*
|
| 297 |
+
* @param {object} props Function properties.
|
| 298 |
+
* @param {Lang[]} props.langs Array of programming languages.
|
| 299 |
+
* @param {number} props.totalSize Total size of all languages.
|
| 300 |
+
* @param {boolean=} props.hideProgress Whether to hide percentage.
|
| 301 |
+
* @param {string=} props.statsFormat Stats format
|
| 302 |
+
* @returns {string} Programming languages SVG node.
|
| 303 |
+
*/
|
| 304 |
+
const createLanguageTextNode = ({
|
| 305 |
+
langs,
|
| 306 |
+
totalSize,
|
| 307 |
+
hideProgress,
|
| 308 |
+
statsFormat,
|
| 309 |
+
}) => {
|
| 310 |
+
const longestLang = getLongestLang(langs);
|
| 311 |
+
const chunked = chunkArray(langs, langs.length / 2);
|
| 312 |
+
const layouts = chunked.map((array) => {
|
| 313 |
+
// @ts-ignore
|
| 314 |
+
const items = array.map((lang, index) =>
|
| 315 |
+
createCompactLangNode({
|
| 316 |
+
lang,
|
| 317 |
+
totalSize,
|
| 318 |
+
hideProgress,
|
| 319 |
+
statsFormat,
|
| 320 |
+
index,
|
| 321 |
+
}),
|
| 322 |
+
);
|
| 323 |
+
return flexLayout({
|
| 324 |
+
items,
|
| 325 |
+
gap: 25,
|
| 326 |
+
direction: "column",
|
| 327 |
+
}).join("");
|
| 328 |
+
});
|
| 329 |
+
|
| 330 |
+
const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
|
| 331 |
+
const minGap = 150;
|
| 332 |
+
const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
|
| 333 |
+
return flexLayout({
|
| 334 |
+
items: layouts,
|
| 335 |
+
gap: maxGap < minGap ? minGap : maxGap,
|
| 336 |
+
}).join("");
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
/**
|
| 340 |
+
* Create donut languages text items for all programming languages.
|
| 341 |
+
*
|
| 342 |
+
* @param {object} props Function properties.
|
| 343 |
+
* @param {Lang[]} props.langs Array of programming languages.
|
| 344 |
+
* @param {number} props.totalSize Total size of all languages.
|
| 345 |
+
* @param {string} props.statsFormat Stats format
|
| 346 |
+
* @returns {string} Donut layout programming language SVG node.
|
| 347 |
+
*/
|
| 348 |
+
const createDonutLanguagesNode = ({ langs, totalSize, statsFormat }) => {
|
| 349 |
+
return flexLayout({
|
| 350 |
+
items: langs.map((lang, index) => {
|
| 351 |
+
return createCompactLangNode({
|
| 352 |
+
lang,
|
| 353 |
+
totalSize,
|
| 354 |
+
hideProgress: false,
|
| 355 |
+
statsFormat,
|
| 356 |
+
index,
|
| 357 |
+
});
|
| 358 |
+
}),
|
| 359 |
+
gap: 32,
|
| 360 |
+
direction: "column",
|
| 361 |
+
}).join("");
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
/**
|
| 365 |
+
* Renders the default language card layout.
|
| 366 |
+
*
|
| 367 |
+
* @param {Lang[]} langs Array of programming languages.
|
| 368 |
+
* @param {number} width Card width.
|
| 369 |
+
* @param {number} totalLanguageSize Total size of all languages.
|
| 370 |
+
* @param {string} statsFormat Stats format.
|
| 371 |
+
* @returns {string} Normal layout card SVG object.
|
| 372 |
+
*/
|
| 373 |
+
const renderNormalLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
| 374 |
+
return flexLayout({
|
| 375 |
+
items: langs.map((lang, index) => {
|
| 376 |
+
return createProgressTextNode({
|
| 377 |
+
width,
|
| 378 |
+
name: lang.name,
|
| 379 |
+
color: lang.color || DEFAULT_LANG_COLOR,
|
| 380 |
+
size: lang.size,
|
| 381 |
+
totalSize: totalLanguageSize,
|
| 382 |
+
statsFormat,
|
| 383 |
+
index,
|
| 384 |
+
});
|
| 385 |
+
}),
|
| 386 |
+
gap: 40,
|
| 387 |
+
direction: "column",
|
| 388 |
+
}).join("");
|
| 389 |
+
};
|
| 390 |
+
|
| 391 |
+
/**
|
| 392 |
+
* Renders the compact language card layout.
|
| 393 |
+
*
|
| 394 |
+
* @param {Lang[]} langs Array of programming languages.
|
| 395 |
+
* @param {number} width Card width.
|
| 396 |
+
* @param {number} totalLanguageSize Total size of all languages.
|
| 397 |
+
* @param {boolean=} hideProgress Whether to hide progress bar.
|
| 398 |
+
* @param {string} statsFormat Stats format.
|
| 399 |
+
* @returns {string} Compact layout card SVG object.
|
| 400 |
+
*/
|
| 401 |
+
const renderCompactLayout = (
|
| 402 |
+
langs,
|
| 403 |
+
width,
|
| 404 |
+
totalLanguageSize,
|
| 405 |
+
hideProgress,
|
| 406 |
+
statsFormat = "percentages",
|
| 407 |
+
) => {
|
| 408 |
+
const paddingRight = 50;
|
| 409 |
+
const offsetWidth = width - paddingRight;
|
| 410 |
+
// progressOffset holds the previous language's width and used to offset the next language
|
| 411 |
+
// so that we can stack them one after another, like this: [--][----][---]
|
| 412 |
+
let progressOffset = 0;
|
| 413 |
+
const compactProgressBar = langs
|
| 414 |
+
.map((lang) => {
|
| 415 |
+
const percentage = parseFloat(
|
| 416 |
+
((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
|
| 417 |
+
);
|
| 418 |
+
|
| 419 |
+
const progress = percentage < 10 ? percentage + 10 : percentage;
|
| 420 |
+
|
| 421 |
+
const output = `
|
| 422 |
+
<rect
|
| 423 |
+
mask="url(#rect-mask)"
|
| 424 |
+
data-testid="lang-progress"
|
| 425 |
+
x="${progressOffset}"
|
| 426 |
+
y="0"
|
| 427 |
+
width="${progress}"
|
| 428 |
+
height="8"
|
| 429 |
+
fill="${lang.color || "#858585"}"
|
| 430 |
+
/>
|
| 431 |
+
`;
|
| 432 |
+
progressOffset += percentage;
|
| 433 |
+
return output;
|
| 434 |
+
})
|
| 435 |
+
.join("");
|
| 436 |
+
|
| 437 |
+
return `
|
| 438 |
+
${
|
| 439 |
+
hideProgress
|
| 440 |
+
? ""
|
| 441 |
+
: `
|
| 442 |
+
<mask id="rect-mask">
|
| 443 |
+
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
| 444 |
+
</mask>
|
| 445 |
+
${compactProgressBar}
|
| 446 |
+
`
|
| 447 |
+
}
|
| 448 |
+
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
| 449 |
+
${createLanguageTextNode({
|
| 450 |
+
langs,
|
| 451 |
+
totalSize: totalLanguageSize,
|
| 452 |
+
hideProgress,
|
| 453 |
+
statsFormat,
|
| 454 |
+
})}
|
| 455 |
+
</g>
|
| 456 |
+
`;
|
| 457 |
+
};
|
| 458 |
+
|
| 459 |
+
/**
|
| 460 |
+
* Renders donut vertical layout to display user's most frequently used programming languages.
|
| 461 |
+
*
|
| 462 |
+
* @param {Lang[]} langs Array of programming languages.
|
| 463 |
+
* @param {number} totalLanguageSize Total size of all languages.
|
| 464 |
+
* @param {string} statsFormat Stats format.
|
| 465 |
+
* @returns {string} Compact layout card SVG object.
|
| 466 |
+
*/
|
| 467 |
+
const renderDonutVerticalLayout = (langs, totalLanguageSize, statsFormat) => {
|
| 468 |
+
// Donut vertical chart radius and total length
|
| 469 |
+
const radius = 80;
|
| 470 |
+
const totalCircleLength = getCircleLength(radius);
|
| 471 |
+
|
| 472 |
+
// SVG circles
|
| 473 |
+
let circles = [];
|
| 474 |
+
|
| 475 |
+
// Start indent for donut vertical chart parts
|
| 476 |
+
let indent = 0;
|
| 477 |
+
|
| 478 |
+
// Start delay coefficient for donut vertical chart parts
|
| 479 |
+
let startDelayCoefficient = 1;
|
| 480 |
+
|
| 481 |
+
// Generate each donut vertical chart part
|
| 482 |
+
for (const lang of langs) {
|
| 483 |
+
const percentage = (lang.size / totalLanguageSize) * 100;
|
| 484 |
+
const circleLength = totalCircleLength * (percentage / 100);
|
| 485 |
+
const delay = startDelayCoefficient * 100;
|
| 486 |
+
|
| 487 |
+
circles.push(`
|
| 488 |
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
| 489 |
+
<circle
|
| 490 |
+
cx="150"
|
| 491 |
+
cy="100"
|
| 492 |
+
r="${radius}"
|
| 493 |
+
fill="transparent"
|
| 494 |
+
stroke="${lang.color}"
|
| 495 |
+
stroke-width="25"
|
| 496 |
+
stroke-dasharray="${totalCircleLength}"
|
| 497 |
+
stroke-dashoffset="${indent}"
|
| 498 |
+
size="${percentage}"
|
| 499 |
+
data-testid="lang-donut"
|
| 500 |
+
/>
|
| 501 |
+
</g>
|
| 502 |
+
`);
|
| 503 |
+
|
| 504 |
+
// Update the indent for the next part
|
| 505 |
+
indent += circleLength;
|
| 506 |
+
// Update the start delay coefficient for the next part
|
| 507 |
+
startDelayCoefficient += 1;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
return `
|
| 511 |
+
<svg data-testid="lang-items">
|
| 512 |
+
<g transform="translate(0, 0)">
|
| 513 |
+
<svg data-testid="donut">
|
| 514 |
+
${circles.join("")}
|
| 515 |
+
</svg>
|
| 516 |
+
</g>
|
| 517 |
+
<g transform="translate(0, 220)">
|
| 518 |
+
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
| 519 |
+
${createLanguageTextNode({
|
| 520 |
+
langs,
|
| 521 |
+
totalSize: totalLanguageSize,
|
| 522 |
+
hideProgress: false,
|
| 523 |
+
statsFormat,
|
| 524 |
+
})}
|
| 525 |
+
</svg>
|
| 526 |
+
</g>
|
| 527 |
+
</svg>
|
| 528 |
+
`;
|
| 529 |
+
};
|
| 530 |
+
|
| 531 |
+
/**
|
| 532 |
+
* Renders pie layout to display user's most frequently used programming languages.
|
| 533 |
+
*
|
| 534 |
+
* @param {Lang[]} langs Array of programming languages.
|
| 535 |
+
* @param {number} totalLanguageSize Total size of all languages.
|
| 536 |
+
* @param {string} statsFormat Stats format.
|
| 537 |
+
* @returns {string} Compact layout card SVG object.
|
| 538 |
+
*/
|
| 539 |
+
const renderPieLayout = (langs, totalLanguageSize, statsFormat) => {
|
| 540 |
+
// Pie chart radius and center coordinates
|
| 541 |
+
const radius = 90;
|
| 542 |
+
const centerX = 150;
|
| 543 |
+
const centerY = 100;
|
| 544 |
+
|
| 545 |
+
// Start angle for the pie chart parts
|
| 546 |
+
let startAngle = 0;
|
| 547 |
+
|
| 548 |
+
// Start delay coefficient for the pie chart parts
|
| 549 |
+
let startDelayCoefficient = 1;
|
| 550 |
+
|
| 551 |
+
// SVG paths
|
| 552 |
+
const paths = [];
|
| 553 |
+
|
| 554 |
+
// Generate each pie chart part
|
| 555 |
+
for (const lang of langs) {
|
| 556 |
+
if (langs.length === 1) {
|
| 557 |
+
paths.push(`
|
| 558 |
+
<circle
|
| 559 |
+
cx="${centerX}"
|
| 560 |
+
cy="${centerY}"
|
| 561 |
+
r="${radius}"
|
| 562 |
+
stroke="none"
|
| 563 |
+
fill="${lang.color}"
|
| 564 |
+
data-testid="lang-pie"
|
| 565 |
+
size="100"
|
| 566 |
+
/>
|
| 567 |
+
`);
|
| 568 |
+
break;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
const langSizePart = lang.size / totalLanguageSize;
|
| 572 |
+
const percentage = langSizePart * 100;
|
| 573 |
+
// Calculate the angle for the current part
|
| 574 |
+
const angle = langSizePart * 360;
|
| 575 |
+
|
| 576 |
+
// Calculate the end angle
|
| 577 |
+
const endAngle = startAngle + angle;
|
| 578 |
+
|
| 579 |
+
// Calculate the coordinates of the start and end points of the arc
|
| 580 |
+
const startPoint = polarToCartesian(centerX, centerY, radius, startAngle);
|
| 581 |
+
const endPoint = polarToCartesian(centerX, centerY, radius, endAngle);
|
| 582 |
+
|
| 583 |
+
// Determine the large arc flag based on the angle
|
| 584 |
+
const largeArcFlag = angle > 180 ? 1 : 0;
|
| 585 |
+
|
| 586 |
+
// Calculate delay
|
| 587 |
+
const delay = startDelayCoefficient * 100;
|
| 588 |
+
|
| 589 |
+
// SVG arc markup
|
| 590 |
+
paths.push(`
|
| 591 |
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
| 592 |
+
<path
|
| 593 |
+
data-testid="lang-pie"
|
| 594 |
+
size="${percentage}"
|
| 595 |
+
d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
|
| 596 |
+
fill="${lang.color}"
|
| 597 |
+
/>
|
| 598 |
+
</g>
|
| 599 |
+
`);
|
| 600 |
+
|
| 601 |
+
// Update the start angle for the next part
|
| 602 |
+
startAngle = endAngle;
|
| 603 |
+
// Update the start delay coefficient for the next part
|
| 604 |
+
startDelayCoefficient += 1;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
return `
|
| 608 |
+
<svg data-testid="lang-items">
|
| 609 |
+
<g transform="translate(0, 0)">
|
| 610 |
+
<svg data-testid="pie">
|
| 611 |
+
${paths.join("")}
|
| 612 |
+
</svg>
|
| 613 |
+
</g>
|
| 614 |
+
<g transform="translate(0, 220)">
|
| 615 |
+
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
| 616 |
+
${createLanguageTextNode({
|
| 617 |
+
langs,
|
| 618 |
+
totalSize: totalLanguageSize,
|
| 619 |
+
hideProgress: false,
|
| 620 |
+
statsFormat,
|
| 621 |
+
})}
|
| 622 |
+
</svg>
|
| 623 |
+
</g>
|
| 624 |
+
</svg>
|
| 625 |
+
`;
|
| 626 |
+
};
|
| 627 |
+
|
| 628 |
+
/**
|
| 629 |
+
* Creates the SVG paths for the language donut chart.
|
| 630 |
+
*
|
| 631 |
+
* @param {number} cx Donut center x-position.
|
| 632 |
+
* @param {number} cy Donut center y-position.
|
| 633 |
+
* @param {number} radius Donut arc Radius.
|
| 634 |
+
* @param {number[]} percentages Array with donut section percentages.
|
| 635 |
+
* @returns {{d: string, percent: number}[]} Array of svg path elements
|
| 636 |
+
*/
|
| 637 |
+
const createDonutPaths = (cx, cy, radius, percentages) => {
|
| 638 |
+
const paths = [];
|
| 639 |
+
let startAngle = 0;
|
| 640 |
+
let endAngle = 0;
|
| 641 |
+
|
| 642 |
+
const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0);
|
| 643 |
+
for (let i = 0; i < percentages.length; i++) {
|
| 644 |
+
const tmpPath = {};
|
| 645 |
+
|
| 646 |
+
let percent = parseFloat(
|
| 647 |
+
((percentages[i] / totalPercent) * 100).toFixed(2),
|
| 648 |
+
);
|
| 649 |
+
|
| 650 |
+
endAngle = 3.6 * percent + startAngle;
|
| 651 |
+
const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
| 652 |
+
const endPoint = polarToCartesian(cx, cy, radius, startAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
| 653 |
+
const largeArc = endAngle - startAngle <= 180 ? 0 : 1;
|
| 654 |
+
|
| 655 |
+
tmpPath.percent = percent;
|
| 656 |
+
tmpPath.d = `M ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArc} 0 ${endPoint.x} ${endPoint.y}`;
|
| 657 |
+
|
| 658 |
+
paths.push(tmpPath);
|
| 659 |
+
startAngle = endAngle;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
return paths;
|
| 663 |
+
};
|
| 664 |
+
|
| 665 |
+
/**
|
| 666 |
+
* Renders the donut language card layout.
|
| 667 |
+
*
|
| 668 |
+
* @param {Lang[]} langs Array of programming languages.
|
| 669 |
+
* @param {number} width Card width.
|
| 670 |
+
* @param {number} totalLanguageSize Total size of all languages.
|
| 671 |
+
* @param {string} statsFormat Stats format.
|
| 672 |
+
* @returns {string} Donut layout card SVG object.
|
| 673 |
+
*/
|
| 674 |
+
const renderDonutLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
| 675 |
+
const centerX = width / 3;
|
| 676 |
+
const centerY = width / 3;
|
| 677 |
+
const radius = centerX - 60;
|
| 678 |
+
const strokeWidth = 12;
|
| 679 |
+
|
| 680 |
+
const colors = langs.map((lang) => lang.color);
|
| 681 |
+
const langsPercents = langs.map((lang) =>
|
| 682 |
+
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
|
| 683 |
+
);
|
| 684 |
+
|
| 685 |
+
const langPaths = createDonutPaths(centerX, centerY, radius, langsPercents);
|
| 686 |
+
|
| 687 |
+
const donutPaths =
|
| 688 |
+
langs.length === 1
|
| 689 |
+
? `<circle cx="${centerX}" cy="${centerY}" r="${radius}" stroke="${colors[0]}" fill="none" stroke-width="${strokeWidth}" data-testid="lang-donut" size="100"/>`
|
| 690 |
+
: langPaths
|
| 691 |
+
.map((section, index) => {
|
| 692 |
+
const staggerDelay = (index + 3) * 100;
|
| 693 |
+
const delay = staggerDelay + 300;
|
| 694 |
+
|
| 695 |
+
const output = `
|
| 696 |
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
| 697 |
+
<path
|
| 698 |
+
data-testid="lang-donut"
|
| 699 |
+
size="${section.percent}"
|
| 700 |
+
d="${section.d}"
|
| 701 |
+
stroke="${colors[index]}"
|
| 702 |
+
fill="none"
|
| 703 |
+
stroke-width="${strokeWidth}">
|
| 704 |
+
</path>
|
| 705 |
+
</g>
|
| 706 |
+
`;
|
| 707 |
+
|
| 708 |
+
return output;
|
| 709 |
+
})
|
| 710 |
+
.join("");
|
| 711 |
+
|
| 712 |
+
const donut = `<svg width="${width}" height="${width}">${donutPaths}</svg>`;
|
| 713 |
+
|
| 714 |
+
return `
|
| 715 |
+
<g transform="translate(0, 0)">
|
| 716 |
+
<g transform="translate(0, 0)">
|
| 717 |
+
${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, statsFormat })}
|
| 718 |
+
</g>
|
| 719 |
+
|
| 720 |
+
<g transform="translate(125, ${donutCenterTranslation(langs.length)})">
|
| 721 |
+
${donut}
|
| 722 |
+
</g>
|
| 723 |
+
</g>
|
| 724 |
+
`;
|
| 725 |
+
};
|
| 726 |
+
|
| 727 |
+
/**
|
| 728 |
+
* @typedef {import("./types").TopLangOptions} TopLangOptions
|
| 729 |
+
* @typedef {TopLangOptions["layout"]} Layout
|
| 730 |
+
*/
|
| 731 |
+
|
| 732 |
+
/**
|
| 733 |
+
* Creates the no languages data SVG node.
|
| 734 |
+
*
|
| 735 |
+
* @param {object} props Object with function properties.
|
| 736 |
+
* @param {string} props.color No languages data text color.
|
| 737 |
+
* @param {string} props.text No languages data translated text.
|
| 738 |
+
* @param {Layout | undefined} props.layout Card layout.
|
| 739 |
+
* @returns {string} No languages data SVG node string.
|
| 740 |
+
*/
|
| 741 |
+
const noLanguagesDataNode = ({ color, text, layout }) => {
|
| 742 |
+
return `
|
| 743 |
+
<text x="${
|
| 744 |
+
layout === "pie" || layout === "donut-vertical" ? CARD_PADDING : 0
|
| 745 |
+
}" y="11" class="stat bold" fill="${color}">${text}</text>
|
| 746 |
+
`;
|
| 747 |
+
};
|
| 748 |
+
|
| 749 |
+
/**
|
| 750 |
+
* Get default languages count for provided card layout.
|
| 751 |
+
*
|
| 752 |
+
* @param {object} props Function properties.
|
| 753 |
+
* @param {Layout=} props.layout Input layout string.
|
| 754 |
+
* @param {boolean=} props.hide_progress Input hide_progress parameter value.
|
| 755 |
+
* @returns {number} Default languages count for input layout.
|
| 756 |
+
*/
|
| 757 |
+
const getDefaultLanguagesCountByLayout = ({ layout, hide_progress }) => {
|
| 758 |
+
if (layout === "compact" || hide_progress === true) {
|
| 759 |
+
return COMPACT_LAYOUT_DEFAULT_LANGS_COUNT;
|
| 760 |
+
} else if (layout === "donut") {
|
| 761 |
+
return DONUT_LAYOUT_DEFAULT_LANGS_COUNT;
|
| 762 |
+
} else if (layout === "donut-vertical") {
|
| 763 |
+
return DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
| 764 |
+
} else if (layout === "pie") {
|
| 765 |
+
return PIE_LAYOUT_DEFAULT_LANGS_COUNT;
|
| 766 |
+
} else {
|
| 767 |
+
return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
| 768 |
+
}
|
| 769 |
+
};
|
| 770 |
+
|
| 771 |
+
/**
|
| 772 |
+
* @typedef {import('../fetchers/types').TopLangData} TopLangData
|
| 773 |
+
*/
|
| 774 |
+
|
| 775 |
+
/**
|
| 776 |
+
* Renders card that display user's most frequently used programming languages.
|
| 777 |
+
*
|
| 778 |
+
* @param {TopLangData} topLangs User's most frequently used programming languages.
|
| 779 |
+
* @param {Partial<TopLangOptions>} options Card options.
|
| 780 |
+
* @returns {string} Language card SVG object.
|
| 781 |
+
*/
|
| 782 |
+
const renderTopLanguages = (topLangs, options = {}) => {
|
| 783 |
+
const {
|
| 784 |
+
hide_title = false,
|
| 785 |
+
hide_border = false,
|
| 786 |
+
card_width,
|
| 787 |
+
title_color,
|
| 788 |
+
text_color,
|
| 789 |
+
bg_color,
|
| 790 |
+
hide,
|
| 791 |
+
hide_progress,
|
| 792 |
+
theme,
|
| 793 |
+
layout,
|
| 794 |
+
custom_title,
|
| 795 |
+
locale,
|
| 796 |
+
langs_count = getDefaultLanguagesCountByLayout({ layout, hide_progress }),
|
| 797 |
+
border_radius,
|
| 798 |
+
border_color,
|
| 799 |
+
disable_animations,
|
| 800 |
+
stats_format = "percentages",
|
| 801 |
+
} = options;
|
| 802 |
+
|
| 803 |
+
const i18n = new I18n({
|
| 804 |
+
locale,
|
| 805 |
+
translations: langCardLocales,
|
| 806 |
+
});
|
| 807 |
+
|
| 808 |
+
const { langs, totalLanguageSize } = trimTopLanguages(
|
| 809 |
+
topLangs,
|
| 810 |
+
langs_count,
|
| 811 |
+
hide,
|
| 812 |
+
);
|
| 813 |
+
|
| 814 |
+
let width = card_width
|
| 815 |
+
? isNaN(card_width)
|
| 816 |
+
? DEFAULT_CARD_WIDTH
|
| 817 |
+
: card_width < MIN_CARD_WIDTH
|
| 818 |
+
? MIN_CARD_WIDTH
|
| 819 |
+
: card_width
|
| 820 |
+
: DEFAULT_CARD_WIDTH;
|
| 821 |
+
let height = calculateNormalLayoutHeight(langs.length);
|
| 822 |
+
|
| 823 |
+
// returns theme based colors with proper overrides and defaults
|
| 824 |
+
const colors = getCardColors({
|
| 825 |
+
title_color,
|
| 826 |
+
text_color,
|
| 827 |
+
bg_color,
|
| 828 |
+
border_color,
|
| 829 |
+
theme,
|
| 830 |
+
});
|
| 831 |
+
|
| 832 |
+
let finalLayout = "";
|
| 833 |
+
if (langs.length === 0) {
|
| 834 |
+
height = COMPACT_LAYOUT_BASE_HEIGHT;
|
| 835 |
+
finalLayout = noLanguagesDataNode({
|
| 836 |
+
color: colors.textColor,
|
| 837 |
+
text: i18n.t("langcard.nodata"),
|
| 838 |
+
layout,
|
| 839 |
+
});
|
| 840 |
+
} else if (layout === "pie") {
|
| 841 |
+
height = calculatePieLayoutHeight(langs.length);
|
| 842 |
+
finalLayout = renderPieLayout(langs, totalLanguageSize, stats_format);
|
| 843 |
+
} else if (layout === "donut-vertical") {
|
| 844 |
+
height = calculateDonutVerticalLayoutHeight(langs.length);
|
| 845 |
+
finalLayout = renderDonutVerticalLayout(
|
| 846 |
+
langs,
|
| 847 |
+
totalLanguageSize,
|
| 848 |
+
stats_format,
|
| 849 |
+
);
|
| 850 |
+
} else if (layout === "compact" || hide_progress == true) {
|
| 851 |
+
height =
|
| 852 |
+
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
| 853 |
+
|
| 854 |
+
finalLayout = renderCompactLayout(
|
| 855 |
+
langs,
|
| 856 |
+
width,
|
| 857 |
+
totalLanguageSize,
|
| 858 |
+
hide_progress,
|
| 859 |
+
stats_format,
|
| 860 |
+
);
|
| 861 |
+
} else if (layout === "donut") {
|
| 862 |
+
height = calculateDonutLayoutHeight(langs.length);
|
| 863 |
+
width = width + 50; // padding
|
| 864 |
+
finalLayout = renderDonutLayout(
|
| 865 |
+
langs,
|
| 866 |
+
width,
|
| 867 |
+
totalLanguageSize,
|
| 868 |
+
stats_format,
|
| 869 |
+
);
|
| 870 |
+
} else {
|
| 871 |
+
finalLayout = renderNormalLayout(
|
| 872 |
+
langs,
|
| 873 |
+
width,
|
| 874 |
+
totalLanguageSize,
|
| 875 |
+
stats_format,
|
| 876 |
+
);
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
const card = new Card({
|
| 880 |
+
customTitle: custom_title,
|
| 881 |
+
defaultTitle: i18n.t("langcard.title"),
|
| 882 |
+
width,
|
| 883 |
+
height,
|
| 884 |
+
border_radius,
|
| 885 |
+
colors,
|
| 886 |
+
});
|
| 887 |
+
|
| 888 |
+
if (disable_animations) {
|
| 889 |
+
card.disableAnimations();
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
card.setHideBorder(hide_border);
|
| 893 |
+
card.setHideTitle(hide_title);
|
| 894 |
+
card.setCSS(
|
| 895 |
+
`
|
| 896 |
+
@keyframes slideInAnimation {
|
| 897 |
+
from {
|
| 898 |
+
width: 0;
|
| 899 |
+
}
|
| 900 |
+
to {
|
| 901 |
+
width: calc(100%-100px);
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
@keyframes growWidthAnimation {
|
| 905 |
+
from {
|
| 906 |
+
width: 0;
|
| 907 |
+
}
|
| 908 |
+
to {
|
| 909 |
+
width: 100%;
|
| 910 |
+
}
|
| 911 |
+
}
|
| 912 |
+
.stat {
|
| 913 |
+
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${colors.textColor};
|
| 914 |
+
}
|
| 915 |
+
@supports(-moz-appearance: auto) {
|
| 916 |
+
/* Selector detects Firefox */
|
| 917 |
+
.stat { font-size:12px; }
|
| 918 |
+
}
|
| 919 |
+
.bold { font-weight: 700 }
|
| 920 |
+
.lang-name {
|
| 921 |
+
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
| 922 |
+
fill: ${colors.textColor};
|
| 923 |
+
}
|
| 924 |
+
.stagger {
|
| 925 |
+
opacity: 0;
|
| 926 |
+
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
| 927 |
+
}
|
| 928 |
+
#rect-mask rect{
|
| 929 |
+
animation: slideInAnimation 1s ease-in-out forwards;
|
| 930 |
+
}
|
| 931 |
+
.lang-progress{
|
| 932 |
+
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
| 933 |
+
}
|
| 934 |
+
`,
|
| 935 |
+
);
|
| 936 |
+
|
| 937 |
+
if (layout === "pie" || layout === "donut-vertical") {
|
| 938 |
+
return card.render(finalLayout);
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
return card.render(`
|
| 942 |
+
<svg data-testid="lang-items" x="${CARD_PADDING}">
|
| 943 |
+
${finalLayout}
|
| 944 |
+
</svg>
|
| 945 |
+
`);
|
| 946 |
+
};
|
| 947 |
+
|
| 948 |
+
export {
|
| 949 |
+
getLongestLang,
|
| 950 |
+
degreesToRadians,
|
| 951 |
+
radiansToDegrees,
|
| 952 |
+
polarToCartesian,
|
| 953 |
+
cartesianToPolar,
|
| 954 |
+
getCircleLength,
|
| 955 |
+
calculateCompactLayoutHeight,
|
| 956 |
+
calculateNormalLayoutHeight,
|
| 957 |
+
calculateDonutLayoutHeight,
|
| 958 |
+
calculateDonutVerticalLayoutHeight,
|
| 959 |
+
calculatePieLayoutHeight,
|
| 960 |
+
donutCenterTranslation,
|
| 961 |
+
trimTopLanguages,
|
| 962 |
+
renderTopLanguages,
|
| 963 |
+
MIN_CARD_WIDTH,
|
| 964 |
+
getDefaultLanguagesCountByLayout,
|
| 965 |
+
};
|
src/cards/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
type ThemeNames = keyof typeof import("../../themes/index.js");
|
| 2 |
+
type RankIcon = "default" | "github" | "percentile";
|
| 3 |
+
|
| 4 |
+
export type CommonOptions = {
|
| 5 |
+
title_color: string;
|
| 6 |
+
icon_color: string;
|
| 7 |
+
text_color: string;
|
| 8 |
+
bg_color: string;
|
| 9 |
+
theme: ThemeNames;
|
| 10 |
+
border_radius: number;
|
| 11 |
+
border_color: string;
|
| 12 |
+
locale: string;
|
| 13 |
+
hide_border: boolean;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export type StatCardOptions = CommonOptions & {
|
| 17 |
+
hide: string[];
|
| 18 |
+
show_icons: boolean;
|
| 19 |
+
hide_title: boolean;
|
| 20 |
+
card_width: number;
|
| 21 |
+
hide_rank: boolean;
|
| 22 |
+
include_all_commits: boolean;
|
| 23 |
+
commits_year: number;
|
| 24 |
+
line_height: number | string;
|
| 25 |
+
custom_title: string;
|
| 26 |
+
disable_animations: boolean;
|
| 27 |
+
number_format: string;
|
| 28 |
+
number_precision: number;
|
| 29 |
+
ring_color: string;
|
| 30 |
+
text_bold: boolean;
|
| 31 |
+
rank_icon: RankIcon;
|
| 32 |
+
show: string[];
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
export type RepoCardOptions = CommonOptions & {
|
| 36 |
+
show_owner: boolean;
|
| 37 |
+
description_lines_count: number;
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
export type TopLangOptions = CommonOptions & {
|
| 41 |
+
hide_title: boolean;
|
| 42 |
+
card_width: number;
|
| 43 |
+
hide: string[];
|
| 44 |
+
layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie";
|
| 45 |
+
custom_title: string;
|
| 46 |
+
langs_count: number;
|
| 47 |
+
disable_animations: boolean;
|
| 48 |
+
hide_progress: boolean;
|
| 49 |
+
stats_format: "percentages" | "bytes";
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
export type WakaTimeOptions = CommonOptions & {
|
| 53 |
+
hide_title: boolean;
|
| 54 |
+
hide: string[];
|
| 55 |
+
card_width: number;
|
| 56 |
+
line_height: string;
|
| 57 |
+
hide_progress: boolean;
|
| 58 |
+
custom_title: string;
|
| 59 |
+
layout: "compact" | "normal";
|
| 60 |
+
langs_count: number;
|
| 61 |
+
display_format: "time" | "percent";
|
| 62 |
+
disable_animations: boolean;
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
export type GistCardOptions = CommonOptions & {
|
| 66 |
+
show_owner: boolean;
|
| 67 |
+
};
|
src/cards/wakatime.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { Card } from "../common/Card.js";
|
| 4 |
+
import { getCardColors } from "../common/color.js";
|
| 5 |
+
import { I18n } from "../common/I18n.js";
|
| 6 |
+
import { clampValue, lowercaseTrim } from "../common/ops.js";
|
| 7 |
+
import { createProgressNode, flexLayout } from "../common/render.js";
|
| 8 |
+
import { wakatimeCardLocales } from "../translations.js";
|
| 9 |
+
|
| 10 |
+
/** Import language colors.
|
| 11 |
+
*
|
| 12 |
+
* @description Here we use the workaround found in
|
| 13 |
+
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
|
| 14 |
+
* since vercel is using v16.14.0 which does not yet support json imports without the
|
| 15 |
+
* --experimental-json-modules flag.
|
| 16 |
+
*/
|
| 17 |
+
import { createRequire } from "module";
|
| 18 |
+
const require = createRequire(import.meta.url);
|
| 19 |
+
const languageColors = require("../common/languageColors.json"); // now works
|
| 20 |
+
|
| 21 |
+
const DEFAULT_CARD_WIDTH = 495;
|
| 22 |
+
const MIN_CARD_WIDTH = 250;
|
| 23 |
+
const COMPACT_LAYOUT_MIN_WIDTH = 400;
|
| 24 |
+
const DEFAULT_LINE_HEIGHT = 25;
|
| 25 |
+
const PROGRESSBAR_PADDING = 130;
|
| 26 |
+
const HIDDEN_PROGRESSBAR_PADDING = 170;
|
| 27 |
+
const COMPACT_LAYOUT_PROGRESSBAR_PADDING = 25;
|
| 28 |
+
const TOTAL_TEXT_WIDTH = 275;
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Creates the no coding activity SVG node.
|
| 32 |
+
*
|
| 33 |
+
* @param {object} props The function properties.
|
| 34 |
+
* @param {string} props.color No coding activity text color.
|
| 35 |
+
* @param {string} props.text No coding activity translated text.
|
| 36 |
+
* @returns {string} No coding activity SVG node string.
|
| 37 |
+
*/
|
| 38 |
+
const noCodingActivityNode = ({ color, text }) => {
|
| 39 |
+
return `
|
| 40 |
+
<text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
|
| 41 |
+
`;
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* @typedef {import('../fetchers/types').WakaTimeLang} WakaTimeLang
|
| 46 |
+
*/
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Format language value.
|
| 50 |
+
*
|
| 51 |
+
* @param {Object} args The function arguments.
|
| 52 |
+
* @param {WakaTimeLang} args.lang The language object.
|
| 53 |
+
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
| 54 |
+
* @returns {string} The formatted language value.
|
| 55 |
+
*/
|
| 56 |
+
const formatLanguageValue = ({ display_format, lang }) => {
|
| 57 |
+
return display_format === "percent"
|
| 58 |
+
? `${lang.percent.toFixed(2).toString()} %`
|
| 59 |
+
: lang.text;
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Create compact WakaTime layout.
|
| 64 |
+
*
|
| 65 |
+
* @param {Object} args The function arguments.
|
| 66 |
+
* @param {WakaTimeLang} args.lang The languages array.
|
| 67 |
+
* @param {number} args.x The x position of the language node.
|
| 68 |
+
* @param {number} args.y The y position of the language node.
|
| 69 |
+
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
| 70 |
+
* @returns {string} The compact layout language SVG node.
|
| 71 |
+
*/
|
| 72 |
+
const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
| 73 |
+
// @ts-ignore
|
| 74 |
+
const color = languageColors[lang.name] || "#858585";
|
| 75 |
+
const value = formatLanguageValue({ display_format, lang });
|
| 76 |
+
|
| 77 |
+
return `
|
| 78 |
+
<g transform="translate(${x}, ${y})">
|
| 79 |
+
<circle cx="5" cy="6" r="5" fill="${color}" />
|
| 80 |
+
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
| 81 |
+
${lang.name} - ${value}
|
| 82 |
+
</text>
|
| 83 |
+
</g>
|
| 84 |
+
`;
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Create WakaTime language text node item.
|
| 89 |
+
*
|
| 90 |
+
* @param {Object} args The function arguments.
|
| 91 |
+
* @param {WakaTimeLang[]} args.langs The language objects.
|
| 92 |
+
* @param {number} args.y The y position of the language node.
|
| 93 |
+
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
| 94 |
+
* @param {number} args.card_width Width in px of the card.
|
| 95 |
+
* @returns {string[]} The language text node items.
|
| 96 |
+
*/
|
| 97 |
+
const createLanguageTextNode = ({ langs, y, display_format, card_width }) => {
|
| 98 |
+
const LEFT_X = 25;
|
| 99 |
+
const RIGHT_X_BASE = 230;
|
| 100 |
+
const rightOffset = (card_width - DEFAULT_CARD_WIDTH) / 2;
|
| 101 |
+
const RIGHT_X = RIGHT_X_BASE + rightOffset;
|
| 102 |
+
|
| 103 |
+
return langs.map((lang, index) => {
|
| 104 |
+
const isLeft = index % 2 === 0;
|
| 105 |
+
return createCompactLangNode({
|
| 106 |
+
lang,
|
| 107 |
+
x: isLeft ? LEFT_X : RIGHT_X,
|
| 108 |
+
y: y + DEFAULT_LINE_HEIGHT * Math.floor(index / 2),
|
| 109 |
+
display_format,
|
| 110 |
+
});
|
| 111 |
+
});
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Create WakaTime text item.
|
| 116 |
+
*
|
| 117 |
+
* @param {Object} args The function arguments.
|
| 118 |
+
* @param {string} args.id The id of the text node item.
|
| 119 |
+
* @param {string} args.label The label of the text node item.
|
| 120 |
+
* @param {string} args.value The value of the text node item.
|
| 121 |
+
* @param {number} args.index The index of the text node item.
|
| 122 |
+
* @param {number} args.percent Percentage of the text node item.
|
| 123 |
+
* @param {boolean=} args.hideProgress Whether to hide the progress bar.
|
| 124 |
+
* @param {string} args.progressBarColor The color of the progress bar.
|
| 125 |
+
* @param {string} args.progressBarBackgroundColor The color of the progress bar background.
|
| 126 |
+
* @param {number} args.progressBarWidth The width of the progress bar.
|
| 127 |
+
* @returns {string} The text SVG node.
|
| 128 |
+
*/
|
| 129 |
+
const createTextNode = ({
|
| 130 |
+
id,
|
| 131 |
+
label,
|
| 132 |
+
value,
|
| 133 |
+
index,
|
| 134 |
+
percent,
|
| 135 |
+
hideProgress,
|
| 136 |
+
progressBarColor,
|
| 137 |
+
progressBarBackgroundColor,
|
| 138 |
+
progressBarWidth,
|
| 139 |
+
}) => {
|
| 140 |
+
const staggerDelay = (index + 3) * 150;
|
| 141 |
+
const cardProgress = hideProgress
|
| 142 |
+
? null
|
| 143 |
+
: createProgressNode({
|
| 144 |
+
x: 110,
|
| 145 |
+
y: 4,
|
| 146 |
+
progress: percent,
|
| 147 |
+
color: progressBarColor,
|
| 148 |
+
width: progressBarWidth,
|
| 149 |
+
// @ts-ignore
|
| 150 |
+
name: label,
|
| 151 |
+
progressBarBackgroundColor,
|
| 152 |
+
delay: staggerDelay + 300,
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
return `
|
| 156 |
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
| 157 |
+
<text class="stat bold" y="12.5" data-testid="${id}">${label}:</text>
|
| 158 |
+
<text
|
| 159 |
+
class="stat"
|
| 160 |
+
x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
|
| 161 |
+
y="12.5"
|
| 162 |
+
>${value}</text>
|
| 163 |
+
${cardProgress}
|
| 164 |
+
</g>
|
| 165 |
+
`;
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Recalculating percentages so that, compact layout's progress bar does not break when
|
| 170 |
+
* hiding languages.
|
| 171 |
+
*
|
| 172 |
+
* @param {WakaTimeLang[]} languages The languages array.
|
| 173 |
+
* @returns {void} The recalculated languages array.
|
| 174 |
+
*/
|
| 175 |
+
const recalculatePercentages = (languages) => {
|
| 176 |
+
const totalSum = languages.reduce(
|
| 177 |
+
(totalSum, language) => totalSum + language.percent,
|
| 178 |
+
0,
|
| 179 |
+
);
|
| 180 |
+
const weight = +(100 / totalSum).toFixed(2);
|
| 181 |
+
languages.forEach((language) => {
|
| 182 |
+
language.percent = +(language.percent * weight).toFixed(2);
|
| 183 |
+
});
|
| 184 |
+
};
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Retrieves CSS styles for a card.
|
| 188 |
+
*
|
| 189 |
+
* @param {Object} colors The colors to use for the card.
|
| 190 |
+
* @param {string} colors.titleColor The title color.
|
| 191 |
+
* @param {string} colors.textColor The text color.
|
| 192 |
+
* @returns {string} Card CSS styles.
|
| 193 |
+
*/
|
| 194 |
+
const getStyles = ({
|
| 195 |
+
// eslint-disable-next-line no-unused-vars
|
| 196 |
+
titleColor,
|
| 197 |
+
textColor,
|
| 198 |
+
}) => {
|
| 199 |
+
return `
|
| 200 |
+
.stat {
|
| 201 |
+
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
| 202 |
+
}
|
| 203 |
+
@supports(-moz-appearance: auto) {
|
| 204 |
+
/* Selector detects Firefox */
|
| 205 |
+
.stat { font-size:12px; }
|
| 206 |
+
}
|
| 207 |
+
.stagger {
|
| 208 |
+
opacity: 0;
|
| 209 |
+
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
| 210 |
+
}
|
| 211 |
+
.not_bold { font-weight: 400 }
|
| 212 |
+
.bold { font-weight: 700 }
|
| 213 |
+
`;
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* Normalize incoming width (string or number) and clamp to minimum.
|
| 218 |
+
*
|
| 219 |
+
* @param {Object} args The function arguments.
|
| 220 |
+
* @param {WakaTimeOptions["layout"] | undefined} args.layout The incoming layout value.
|
| 221 |
+
* @param {number|undefined} args.value The incoming width value.
|
| 222 |
+
* @returns {number} The normalized width value.
|
| 223 |
+
*/
|
| 224 |
+
const normalizeCardWidth = ({ value, layout }) => {
|
| 225 |
+
if (value === undefined || value === null || isNaN(value)) {
|
| 226 |
+
return DEFAULT_CARD_WIDTH;
|
| 227 |
+
}
|
| 228 |
+
return Math.max(
|
| 229 |
+
layout === "compact" ? COMPACT_LAYOUT_MIN_WIDTH : MIN_CARD_WIDTH,
|
| 230 |
+
value,
|
| 231 |
+
);
|
| 232 |
+
};
|
| 233 |
+
|
| 234 |
+
/**
|
| 235 |
+
* @typedef {import('../fetchers/types').WakaTimeData} WakaTimeData
|
| 236 |
+
* @typedef {import('./types').WakaTimeOptions} WakaTimeOptions
|
| 237 |
+
*/
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* Renders WakaTime card.
|
| 241 |
+
*
|
| 242 |
+
* @param {Partial<WakaTimeData>} stats WakaTime stats.
|
| 243 |
+
* @param {Partial<WakaTimeOptions>} options Card options.
|
| 244 |
+
* @returns {string} WakaTime card SVG.
|
| 245 |
+
*/
|
| 246 |
+
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
| 247 |
+
let { languages = [] } = stats;
|
| 248 |
+
const {
|
| 249 |
+
hide_title = false,
|
| 250 |
+
hide_border = false,
|
| 251 |
+
card_width,
|
| 252 |
+
hide,
|
| 253 |
+
line_height = DEFAULT_LINE_HEIGHT,
|
| 254 |
+
title_color,
|
| 255 |
+
icon_color,
|
| 256 |
+
text_color,
|
| 257 |
+
bg_color,
|
| 258 |
+
theme = "default",
|
| 259 |
+
hide_progress,
|
| 260 |
+
custom_title,
|
| 261 |
+
locale,
|
| 262 |
+
layout,
|
| 263 |
+
langs_count = languages.length,
|
| 264 |
+
border_radius,
|
| 265 |
+
border_color,
|
| 266 |
+
display_format = "time",
|
| 267 |
+
disable_animations,
|
| 268 |
+
} = options;
|
| 269 |
+
|
| 270 |
+
const normalizedWidth = normalizeCardWidth({ value: card_width, layout });
|
| 271 |
+
|
| 272 |
+
const shouldHideLangs = Array.isArray(hide) && hide.length > 0;
|
| 273 |
+
if (shouldHideLangs) {
|
| 274 |
+
const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang)));
|
| 275 |
+
languages = languages.filter(
|
| 276 |
+
(lang) => !languagesToHide.has(lowercaseTrim(lang.name)),
|
| 277 |
+
);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// Since the percentages are sorted in descending order, we can just
|
| 281 |
+
// slice from the beginning without sorting.
|
| 282 |
+
languages = languages.slice(0, langs_count);
|
| 283 |
+
recalculatePercentages(languages);
|
| 284 |
+
|
| 285 |
+
const i18n = new I18n({
|
| 286 |
+
locale,
|
| 287 |
+
translations: wakatimeCardLocales,
|
| 288 |
+
});
|
| 289 |
+
|
| 290 |
+
const lheight = parseInt(String(line_height), 10);
|
| 291 |
+
|
| 292 |
+
const langsCount = clampValue(langs_count, 1, langs_count);
|
| 293 |
+
|
| 294 |
+
// returns theme based colors with proper overrides and defaults
|
| 295 |
+
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
| 296 |
+
getCardColors({
|
| 297 |
+
title_color,
|
| 298 |
+
icon_color,
|
| 299 |
+
text_color,
|
| 300 |
+
bg_color,
|
| 301 |
+
border_color,
|
| 302 |
+
theme,
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
const filteredLanguages = languages
|
| 306 |
+
.filter((language) => language.hours || language.minutes)
|
| 307 |
+
.slice(0, langsCount);
|
| 308 |
+
|
| 309 |
+
// Calculate the card height depending on how many items there are
|
| 310 |
+
// but if rank circle is visible clamp the minimum height to `150`
|
| 311 |
+
let height = Math.max(45 + (filteredLanguages.length + 1) * lheight, 150);
|
| 312 |
+
|
| 313 |
+
const cssStyles = getStyles({
|
| 314 |
+
titleColor,
|
| 315 |
+
textColor,
|
| 316 |
+
});
|
| 317 |
+
|
| 318 |
+
let finalLayout = "";
|
| 319 |
+
|
| 320 |
+
// RENDER COMPACT LAYOUT
|
| 321 |
+
if (layout === "compact") {
|
| 322 |
+
const width = normalizedWidth - 5;
|
| 323 |
+
height =
|
| 324 |
+
90 + Math.round(filteredLanguages.length / 2) * DEFAULT_LINE_HEIGHT;
|
| 325 |
+
|
| 326 |
+
// progressOffset holds the previous language's width and used to offset the next language
|
| 327 |
+
// so that we can stack them one after another, like this: [--][----][---]
|
| 328 |
+
let progressOffset = 0;
|
| 329 |
+
const compactProgressBar = filteredLanguages
|
| 330 |
+
.map((language) => {
|
| 331 |
+
const progress =
|
| 332 |
+
((width - COMPACT_LAYOUT_PROGRESSBAR_PADDING) * language.percent) /
|
| 333 |
+
100;
|
| 334 |
+
|
| 335 |
+
// @ts-ignore
|
| 336 |
+
const languageColor = languageColors[language.name] || "#858585";
|
| 337 |
+
|
| 338 |
+
const output = `
|
| 339 |
+
<rect
|
| 340 |
+
mask="url(#rect-mask)"
|
| 341 |
+
data-testid="lang-progress"
|
| 342 |
+
x="${progressOffset}"
|
| 343 |
+
y="0"
|
| 344 |
+
width="${progress}"
|
| 345 |
+
height="8"
|
| 346 |
+
fill="${languageColor}"
|
| 347 |
+
/>
|
| 348 |
+
`;
|
| 349 |
+
progressOffset += progress;
|
| 350 |
+
return output;
|
| 351 |
+
})
|
| 352 |
+
.join("");
|
| 353 |
+
|
| 354 |
+
finalLayout = `
|
| 355 |
+
<mask id="rect-mask">
|
| 356 |
+
<rect x="${COMPACT_LAYOUT_PROGRESSBAR_PADDING}" y="0" width="${width - 2 * COMPACT_LAYOUT_PROGRESSBAR_PADDING}" height="8" fill="white" rx="5" />
|
| 357 |
+
</mask>
|
| 358 |
+
${compactProgressBar}
|
| 359 |
+
${
|
| 360 |
+
filteredLanguages.length
|
| 361 |
+
? createLanguageTextNode({
|
| 362 |
+
y: 25,
|
| 363 |
+
langs: filteredLanguages,
|
| 364 |
+
display_format,
|
| 365 |
+
card_width: normalizedWidth,
|
| 366 |
+
}).join("")
|
| 367 |
+
: noCodingActivityNode({
|
| 368 |
+
// @ts-ignore
|
| 369 |
+
color: textColor,
|
| 370 |
+
text: stats.is_coding_activity_visible
|
| 371 |
+
? stats.is_other_usage_visible
|
| 372 |
+
? i18n.t("wakatimecard.nocodingactivity")
|
| 373 |
+
: i18n.t("wakatimecard.nocodedetails")
|
| 374 |
+
: i18n.t("wakatimecard.notpublic"),
|
| 375 |
+
})
|
| 376 |
+
}
|
| 377 |
+
`;
|
| 378 |
+
} else {
|
| 379 |
+
finalLayout = flexLayout({
|
| 380 |
+
items: filteredLanguages.length
|
| 381 |
+
? filteredLanguages.map((language, index) => {
|
| 382 |
+
return createTextNode({
|
| 383 |
+
id: language.name,
|
| 384 |
+
label: language.name,
|
| 385 |
+
value: formatLanguageValue({ display_format, lang: language }),
|
| 386 |
+
index,
|
| 387 |
+
percent: language.percent,
|
| 388 |
+
// @ts-ignore
|
| 389 |
+
progressBarColor: titleColor,
|
| 390 |
+
// @ts-ignore
|
| 391 |
+
progressBarBackgroundColor: textColor,
|
| 392 |
+
hideProgress: hide_progress,
|
| 393 |
+
progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
|
| 394 |
+
});
|
| 395 |
+
})
|
| 396 |
+
: [
|
| 397 |
+
noCodingActivityNode({
|
| 398 |
+
// @ts-ignore
|
| 399 |
+
color: textColor,
|
| 400 |
+
text: stats.is_coding_activity_visible
|
| 401 |
+
? stats.is_other_usage_visible
|
| 402 |
+
? i18n.t("wakatimecard.nocodingactivity")
|
| 403 |
+
: i18n.t("wakatimecard.nocodedetails")
|
| 404 |
+
: i18n.t("wakatimecard.notpublic"),
|
| 405 |
+
}),
|
| 406 |
+
],
|
| 407 |
+
gap: lheight,
|
| 408 |
+
direction: "column",
|
| 409 |
+
}).join("");
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Get title range text
|
| 413 |
+
let titleText = i18n.t("wakatimecard.title");
|
| 414 |
+
switch (stats.range) {
|
| 415 |
+
case "last_7_days":
|
| 416 |
+
titleText += ` (${i18n.t("wakatimecard.last7days")})`;
|
| 417 |
+
break;
|
| 418 |
+
case "last_year":
|
| 419 |
+
titleText += ` (${i18n.t("wakatimecard.lastyear")})`;
|
| 420 |
+
break;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
const card = new Card({
|
| 424 |
+
customTitle: custom_title,
|
| 425 |
+
defaultTitle: titleText,
|
| 426 |
+
width: normalizedWidth,
|
| 427 |
+
height,
|
| 428 |
+
border_radius,
|
| 429 |
+
colors: {
|
| 430 |
+
titleColor,
|
| 431 |
+
textColor,
|
| 432 |
+
iconColor,
|
| 433 |
+
bgColor,
|
| 434 |
+
borderColor,
|
| 435 |
+
},
|
| 436 |
+
});
|
| 437 |
+
|
| 438 |
+
if (disable_animations) {
|
| 439 |
+
card.disableAnimations();
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
card.setHideBorder(hide_border);
|
| 443 |
+
card.setHideTitle(hide_title);
|
| 444 |
+
card.setCSS(
|
| 445 |
+
`
|
| 446 |
+
${cssStyles}
|
| 447 |
+
@keyframes slideInAnimation {
|
| 448 |
+
from {
|
| 449 |
+
width: 0;
|
| 450 |
+
}
|
| 451 |
+
to {
|
| 452 |
+
width: calc(100%-100px);
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
@keyframes growWidthAnimation {
|
| 456 |
+
from {
|
| 457 |
+
width: 0;
|
| 458 |
+
}
|
| 459 |
+
to {
|
| 460 |
+
width: 100%;
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
| 464 |
+
#rect-mask rect{
|
| 465 |
+
animation: slideInAnimation 1s ease-in-out forwards;
|
| 466 |
+
}
|
| 467 |
+
.lang-progress{
|
| 468 |
+
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
| 469 |
+
}
|
| 470 |
+
`,
|
| 471 |
+
);
|
| 472 |
+
|
| 473 |
+
return card.render(`
|
| 474 |
+
<svg x="0" y="0" width="100%">
|
| 475 |
+
${finalLayout}
|
| 476 |
+
</svg>
|
| 477 |
+
`);
|
| 478 |
+
};
|
| 479 |
+
|
| 480 |
+
export { renderWakatimeCard };
|
| 481 |
+
export default renderWakatimeCard;
|
src/common/Card.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { encodeHTML } 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 |
+
* @param {string} body The inner body of the card.
|
| 206 |
+
* @returns {string} The rendered card.
|
| 207 |
+
*/
|
| 208 |
+
render(body) {
|
| 209 |
+
return `
|
| 210 |
+
<svg
|
| 211 |
+
width="${this.width}"
|
| 212 |
+
height="${this.height}"
|
| 213 |
+
viewBox="0 0 ${this.width} ${this.height}"
|
| 214 |
+
fill="none"
|
| 215 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 216 |
+
role="img"
|
| 217 |
+
aria-labelledby="descId"
|
| 218 |
+
>
|
| 219 |
+
<title id="titleId">${this.a11yTitle}</title>
|
| 220 |
+
<desc id="descId">${this.a11yDesc}</desc>
|
| 221 |
+
<style>
|
| 222 |
+
.header {
|
| 223 |
+
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
| 224 |
+
fill: ${this.colors.titleColor};
|
| 225 |
+
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
| 226 |
+
}
|
| 227 |
+
@supports(-moz-appearance: auto) {
|
| 228 |
+
/* Selector detects Firefox */
|
| 229 |
+
.header { font-size: 15.5px; }
|
| 230 |
+
}
|
| 231 |
+
${this.css}
|
| 232 |
+
|
| 233 |
+
${process.env.NODE_ENV === "test" ? "" : this.getAnimations()}
|
| 234 |
+
${
|
| 235 |
+
this.animations === false
|
| 236 |
+
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
| 237 |
+
: ""
|
| 238 |
+
}
|
| 239 |
+
</style>
|
| 240 |
+
|
| 241 |
+
${this.renderGradient()}
|
| 242 |
+
|
| 243 |
+
<rect
|
| 244 |
+
data-testid="card-bg"
|
| 245 |
+
x="0.5"
|
| 246 |
+
y="0.5"
|
| 247 |
+
rx="${this.border_radius}"
|
| 248 |
+
height="99%"
|
| 249 |
+
stroke="${this.colors.borderColor}"
|
| 250 |
+
width="${this.width - 1}"
|
| 251 |
+
fill="${
|
| 252 |
+
typeof this.colors.bgColor === "object"
|
| 253 |
+
? "url(#gradient)"
|
| 254 |
+
: this.colors.bgColor
|
| 255 |
+
}"
|
| 256 |
+
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
| 257 |
+
/>
|
| 258 |
+
|
| 259 |
+
${this.hideTitle ? "" : this.renderTitle()}
|
| 260 |
+
|
| 261 |
+
<g
|
| 262 |
+
data-testid="main-card-body"
|
| 263 |
+
transform="translate(0, ${
|
| 264 |
+
this.hideTitle ? this.paddingX : this.paddingY + 20
|
| 265 |
+
})"
|
| 266 |
+
>
|
| 267 |
+
${body}
|
| 268 |
+
</g>
|
| 269 |
+
</svg>
|
| 270 |
+
`;
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
export { Card };
|
| 275 |
+
export default Card;
|
src/common/I18n.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 34 |
+
`'${str}' translation not found for locale '${this.locale}'`,
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return this.translations[str][this.locale];
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export { I18n };
|
| 43 |
+
export default I18n;
|
src/common/access.js
ADDED
|
@@ -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 };
|
src/common/blacklist.js
ADDED
|
@@ -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;
|
src/common/cache.js
ADDED
|
@@ -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 |
+
};
|
src/common/color.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
export { isValidHexColor, isValidGradient, getCardColors };
|
src/common/envs.js
ADDED
|
@@ -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 };
|
src/common/error.js
ADDED
|
@@ -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 |
+
};
|
src/common/fmt.js
ADDED
|
@@ -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 };
|
src/common/html.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Encode string as HTML.
|
| 5 |
+
*
|
| 6 |
+
* @see https://stackoverflow.com/a/48073476/10629172
|
| 7 |
+
*
|
| 8 |
+
* @param {string} str String to encode.
|
| 9 |
+
* @returns {string} Encoded string.
|
| 10 |
+
*/
|
| 11 |
+
const encodeHTML = (str) => {
|
| 12 |
+
return str
|
| 13 |
+
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
| 14 |
+
return "&#" + i.charCodeAt(0) + ";";
|
| 15 |
+
})
|
| 16 |
+
.replace(/\u0008/gim, "");
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
export { encodeHTML };
|
src/common/http.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
data,
|
| 18 |
+
});
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export { request };
|
src/common/icons.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
const icons = {
|
| 4 |
+
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"/>`,
|
| 5 |
+
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"/>`,
|
| 6 |
+
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"/>`,
|
| 7 |
+
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" />`,
|
| 8 |
+
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" />`,
|
| 9 |
+
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"/>`,
|
| 10 |
+
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"/>`,
|
| 11 |
+
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"/>`,
|
| 12 |
+
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>`,
|
| 13 |
+
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"/>`,
|
| 14 |
+
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" />`,
|
| 15 |
+
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" />`,
|
| 16 |
+
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" />`,
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Get rank icon
|
| 21 |
+
*
|
| 22 |
+
* @param {string} rankIcon - The rank icon type.
|
| 23 |
+
* @param {string} rankLevel - The rank level.
|
| 24 |
+
* @param {number} percentile - The rank percentile.
|
| 25 |
+
* @returns {string} - The SVG code of the rank icon
|
| 26 |
+
*/
|
| 27 |
+
const rankIcon = (rankIcon, rankLevel, percentile) => {
|
| 28 |
+
switch (rankIcon) {
|
| 29 |
+
case "github":
|
| 30 |
+
return `
|
| 31 |
+
<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">
|
| 32 |
+
<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>
|
| 33 |
+
</svg>
|
| 34 |
+
`;
|
| 35 |
+
case "percentile":
|
| 36 |
+
return `
|
| 37 |
+
<text x="-5" y="-12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-top-header" class="rank-percentile-header">
|
| 38 |
+
Top
|
| 39 |
+
</text>
|
| 40 |
+
<text x="-5" y="12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-rank-value" class="rank-percentile-text">
|
| 41 |
+
${percentile.toFixed(1)}%
|
| 42 |
+
</text>
|
| 43 |
+
`;
|
| 44 |
+
case "default":
|
| 45 |
+
default:
|
| 46 |
+
return `
|
| 47 |
+
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
|
| 48 |
+
${rankLevel}
|
| 49 |
+
</text>
|
| 50 |
+
`;
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
export { icons, rankIcon };
|
| 55 |
+
export default icons;
|
src/common/index.js
ADDED
|
@@ -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";
|
src/common/languageColors.json
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1C Enterprise": "#814CCC",
|
| 3 |
+
"2-Dimensional Array": "#38761D",
|
| 4 |
+
"4D": "#004289",
|
| 5 |
+
"ABAP": "#E8274B",
|
| 6 |
+
"ABAP CDS": "#555e25",
|
| 7 |
+
"AGS Script": "#B9D9FF",
|
| 8 |
+
"AIDL": "#34EB6B",
|
| 9 |
+
"AL": "#3AA2B5",
|
| 10 |
+
"AMPL": "#E6EFBB",
|
| 11 |
+
"ANTLR": "#9DC3FF",
|
| 12 |
+
"API Blueprint": "#2ACCA8",
|
| 13 |
+
"APL": "#5A8164",
|
| 14 |
+
"ASP.NET": "#9400ff",
|
| 15 |
+
"ATS": "#1ac620",
|
| 16 |
+
"ActionScript": "#882B0F",
|
| 17 |
+
"Ada": "#02f88c",
|
| 18 |
+
"Adblock Filter List": "#800000",
|
| 19 |
+
"Adobe Font Metrics": "#fa0f00",
|
| 20 |
+
"Agda": "#315665",
|
| 21 |
+
"Aiken": "#640ff8",
|
| 22 |
+
"Alloy": "#64C800",
|
| 23 |
+
"Alpine Abuild": "#0D597F",
|
| 24 |
+
"Altium Designer": "#A89663",
|
| 25 |
+
"AngelScript": "#C7D7DC",
|
| 26 |
+
"Answer Set Programming": "#A9CC29",
|
| 27 |
+
"Ant Build System": "#A9157E",
|
| 28 |
+
"Antlers": "#ff269e",
|
| 29 |
+
"ApacheConf": "#d12127",
|
| 30 |
+
"Apex": "#1797c0",
|
| 31 |
+
"Apollo Guidance Computer": "#0B3D91",
|
| 32 |
+
"AppleScript": "#101F1F",
|
| 33 |
+
"Arc": "#aa2afe",
|
| 34 |
+
"ArkTS": "#0080ff",
|
| 35 |
+
"AsciiDoc": "#73a0c5",
|
| 36 |
+
"AspectJ": "#a957b0",
|
| 37 |
+
"Assembly": "#6E4C13",
|
| 38 |
+
"Astro": "#ff5a03",
|
| 39 |
+
"Asymptote": "#ff0000",
|
| 40 |
+
"Augeas": "#9CC134",
|
| 41 |
+
"AutoHotkey": "#6594b9",
|
| 42 |
+
"AutoIt": "#1C3552",
|
| 43 |
+
"Avro IDL": "#0040FF",
|
| 44 |
+
"Awk": "#c30e9b",
|
| 45 |
+
"B4X": "#00e4ff",
|
| 46 |
+
"BASIC": "#ff0000",
|
| 47 |
+
"BQN": "#2b7067",
|
| 48 |
+
"Ballerina": "#FF5000",
|
| 49 |
+
"Batchfile": "#C1F12E",
|
| 50 |
+
"Beef": "#a52f4e",
|
| 51 |
+
"Berry": "#15A13C",
|
| 52 |
+
"BibTeX": "#778899",
|
| 53 |
+
"Bicep": "#519aba",
|
| 54 |
+
"Bikeshed": "#5562ac",
|
| 55 |
+
"Bison": "#6A463F",
|
| 56 |
+
"BitBake": "#00bce4",
|
| 57 |
+
"Blade": "#f7523f",
|
| 58 |
+
"BlitzBasic": "#00FFAE",
|
| 59 |
+
"BlitzMax": "#cd6400",
|
| 60 |
+
"Bluespec": "#12223c",
|
| 61 |
+
"Bluespec BH": "#12223c",
|
| 62 |
+
"Boo": "#d4bec1",
|
| 63 |
+
"Boogie": "#c80fa0",
|
| 64 |
+
"Brainfuck": "#2F2530",
|
| 65 |
+
"BrighterScript": "#66AABB",
|
| 66 |
+
"Brightscript": "#662D91",
|
| 67 |
+
"Browserslist": "#ffd539",
|
| 68 |
+
"Bru": "#F4AA41",
|
| 69 |
+
"BuildStream": "#006bff",
|
| 70 |
+
"C": "#555555",
|
| 71 |
+
"C#": "#178600",
|
| 72 |
+
"C++": "#f34b7d",
|
| 73 |
+
"C3": "#2563eb",
|
| 74 |
+
"CAP CDS": "#0092d1",
|
| 75 |
+
"CLIPS": "#00A300",
|
| 76 |
+
"CMake": "#DA3434",
|
| 77 |
+
"COLLADA": "#F1A42B",
|
| 78 |
+
"CSON": "#244776",
|
| 79 |
+
"CSS": "#663399",
|
| 80 |
+
"CSV": "#237346",
|
| 81 |
+
"CUE": "#5886E1",
|
| 82 |
+
"CWeb": "#00007a",
|
| 83 |
+
"Cabal Config": "#483465",
|
| 84 |
+
"Caddyfile": "#22b638",
|
| 85 |
+
"Cadence": "#00ef8b",
|
| 86 |
+
"Cairo": "#ff4a48",
|
| 87 |
+
"Cairo Zero": "#ff4a48",
|
| 88 |
+
"CameLIGO": "#3be133",
|
| 89 |
+
"Cangjie": "#00868B",
|
| 90 |
+
"Cap'n Proto": "#c42727",
|
| 91 |
+
"Carbon": "#222222",
|
| 92 |
+
"Ceylon": "#dfa535",
|
| 93 |
+
"Chapel": "#8dc63f",
|
| 94 |
+
"ChucK": "#3f8000",
|
| 95 |
+
"Circom": "#707575",
|
| 96 |
+
"Cirru": "#ccccff",
|
| 97 |
+
"Clarion": "#db901e",
|
| 98 |
+
"Clarity": "#5546ff",
|
| 99 |
+
"Classic ASP": "#6a40fd",
|
| 100 |
+
"Clean": "#3F85AF",
|
| 101 |
+
"Click": "#E4E6F3",
|
| 102 |
+
"Clojure": "#db5855",
|
| 103 |
+
"Closure Templates": "#0d948f",
|
| 104 |
+
"Cloud Firestore Security Rules": "#FFA000",
|
| 105 |
+
"Clue": "#0009b5",
|
| 106 |
+
"CodeQL": "#140f46",
|
| 107 |
+
"CoffeeScript": "#244776",
|
| 108 |
+
"ColdFusion": "#ed2cd6",
|
| 109 |
+
"ColdFusion CFC": "#ed2cd6",
|
| 110 |
+
"Common Lisp": "#3fb68b",
|
| 111 |
+
"Common Workflow Language": "#B5314C",
|
| 112 |
+
"Component Pascal": "#B0CE4E",
|
| 113 |
+
"Cooklang": "#E15A29",
|
| 114 |
+
"Crystal": "#000100",
|
| 115 |
+
"Csound": "#1a1a1a",
|
| 116 |
+
"Csound Document": "#1a1a1a",
|
| 117 |
+
"Csound Score": "#1a1a1a",
|
| 118 |
+
"Cuda": "#3A4E3A",
|
| 119 |
+
"Curry": "#531242",
|
| 120 |
+
"Cylc": "#00b3fd",
|
| 121 |
+
"Cypher": "#34c0eb",
|
| 122 |
+
"Cython": "#fedf5b",
|
| 123 |
+
"D": "#ba595e",
|
| 124 |
+
"D2": "#526ee8",
|
| 125 |
+
"DM": "#447265",
|
| 126 |
+
"Dafny": "#FFEC25",
|
| 127 |
+
"Darcs Patch": "#8eff23",
|
| 128 |
+
"Dart": "#00B4AB",
|
| 129 |
+
"Daslang": "#d3d3d3",
|
| 130 |
+
"DataWeave": "#003a52",
|
| 131 |
+
"Debian Package Control File": "#D70751",
|
| 132 |
+
"DenizenScript": "#FBEE96",
|
| 133 |
+
"Dhall": "#dfafff",
|
| 134 |
+
"DirectX 3D File": "#aace60",
|
| 135 |
+
"Dockerfile": "#384d54",
|
| 136 |
+
"Dogescript": "#cca760",
|
| 137 |
+
"Dotenv": "#e5d559",
|
| 138 |
+
"Dune": "#89421e",
|
| 139 |
+
"Dylan": "#6c616e",
|
| 140 |
+
"E": "#ccce35",
|
| 141 |
+
"ECL": "#8a1267",
|
| 142 |
+
"ECLiPSe": "#001d9d",
|
| 143 |
+
"EJS": "#a91e50",
|
| 144 |
+
"EQ": "#a78649",
|
| 145 |
+
"Earthly": "#2af0ff",
|
| 146 |
+
"Easybuild": "#069406",
|
| 147 |
+
"Ecere Projects": "#913960",
|
| 148 |
+
"Ecmarkup": "#eb8131",
|
| 149 |
+
"Edge": "#0dffe0",
|
| 150 |
+
"EdgeQL": "#31A7FF",
|
| 151 |
+
"EditorConfig": "#fff1f2",
|
| 152 |
+
"Eiffel": "#4d6977",
|
| 153 |
+
"Elixir": "#6e4a7e",
|
| 154 |
+
"Elm": "#60B5CC",
|
| 155 |
+
"Elvish": "#55BB55",
|
| 156 |
+
"Elvish Transcript": "#55BB55",
|
| 157 |
+
"Emacs Lisp": "#c065db",
|
| 158 |
+
"EmberScript": "#FFF4F3",
|
| 159 |
+
"Erlang": "#B83998",
|
| 160 |
+
"Euphoria": "#FF790B",
|
| 161 |
+
"F#": "#b845fc",
|
| 162 |
+
"F*": "#572e30",
|
| 163 |
+
"FIGlet Font": "#FFDDBB",
|
| 164 |
+
"FIRRTL": "#2f632f",
|
| 165 |
+
"FLUX": "#88ccff",
|
| 166 |
+
"Factor": "#636746",
|
| 167 |
+
"Fancy": "#7b9db4",
|
| 168 |
+
"Fantom": "#14253c",
|
| 169 |
+
"Faust": "#c37240",
|
| 170 |
+
"Fennel": "#fff3d7",
|
| 171 |
+
"Filebench WML": "#F6B900",
|
| 172 |
+
"Flix": "#d44a45",
|
| 173 |
+
"Fluent": "#ffcc33",
|
| 174 |
+
"Forth": "#341708",
|
| 175 |
+
"Fortran": "#4d41b1",
|
| 176 |
+
"Fortran Free Form": "#4d41b1",
|
| 177 |
+
"FreeBASIC": "#141AC9",
|
| 178 |
+
"FreeMarker": "#0050b2",
|
| 179 |
+
"Frege": "#00cafe",
|
| 180 |
+
"Futhark": "#5f021f",
|
| 181 |
+
"G-code": "#D08CF2",
|
| 182 |
+
"GAML": "#FFC766",
|
| 183 |
+
"GAMS": "#f49a22",
|
| 184 |
+
"GAP": "#0000cc",
|
| 185 |
+
"GCC Machine Description": "#FFCFAB",
|
| 186 |
+
"GDScript": "#355570",
|
| 187 |
+
"GDShader": "#478CBF",
|
| 188 |
+
"GEDCOM": "#003058",
|
| 189 |
+
"GLSL": "#5686a5",
|
| 190 |
+
"GSC": "#FF6800",
|
| 191 |
+
"Game Maker Language": "#71b417",
|
| 192 |
+
"Gemfile.lock": "#701516",
|
| 193 |
+
"Gemini": "#ff6900",
|
| 194 |
+
"Genero 4gl": "#63408e",
|
| 195 |
+
"Genero per": "#d8df39",
|
| 196 |
+
"Genie": "#fb855d",
|
| 197 |
+
"Genshi": "#951531",
|
| 198 |
+
"Gentoo Ebuild": "#9400ff",
|
| 199 |
+
"Gentoo Eclass": "#9400ff",
|
| 200 |
+
"Gerber Image": "#d20b00",
|
| 201 |
+
"Gherkin": "#5B2063",
|
| 202 |
+
"Git Attributes": "#F44D27",
|
| 203 |
+
"Git Commit": "#F44D27",
|
| 204 |
+
"Git Config": "#F44D27",
|
| 205 |
+
"Git Revision List": "#F44D27",
|
| 206 |
+
"Gleam": "#ffaff3",
|
| 207 |
+
"Glimmer JS": "#F5835F",
|
| 208 |
+
"Glimmer TS": "#3178c6",
|
| 209 |
+
"Glyph": "#c1ac7f",
|
| 210 |
+
"Gnuplot": "#f0a9f0",
|
| 211 |
+
"Go": "#00ADD8",
|
| 212 |
+
"Go Checksums": "#00ADD8",
|
| 213 |
+
"Go Module": "#00ADD8",
|
| 214 |
+
"Go Workspace": "#00ADD8",
|
| 215 |
+
"Godot Resource": "#355570",
|
| 216 |
+
"Golo": "#88562A",
|
| 217 |
+
"Gosu": "#82937f",
|
| 218 |
+
"Grace": "#615f8b",
|
| 219 |
+
"Gradle": "#02303a",
|
| 220 |
+
"Gradle Kotlin DSL": "#02303a",
|
| 221 |
+
"Grammatical Framework": "#ff0000",
|
| 222 |
+
"GraphQL": "#e10098",
|
| 223 |
+
"Graphviz (DOT)": "#2596be",
|
| 224 |
+
"Groovy": "#4298b8",
|
| 225 |
+
"Groovy Server Pages": "#4298b8",
|
| 226 |
+
"HAProxy": "#106da9",
|
| 227 |
+
"HCL": "#844FBA",
|
| 228 |
+
"HIP": "#4F3A4F",
|
| 229 |
+
"HLSL": "#aace60",
|
| 230 |
+
"HOCON": "#9ff8ee",
|
| 231 |
+
"HTML": "#e34c26",
|
| 232 |
+
"HTML+ECR": "#2e1052",
|
| 233 |
+
"HTML+EEX": "#6e4a7e",
|
| 234 |
+
"HTML+ERB": "#701516",
|
| 235 |
+
"HTML+PHP": "#4f5d95",
|
| 236 |
+
"HTML+Razor": "#512be4",
|
| 237 |
+
"HTTP": "#005C9C",
|
| 238 |
+
"HXML": "#f68712",
|
| 239 |
+
"Hack": "#878787",
|
| 240 |
+
"Haml": "#ece2a9",
|
| 241 |
+
"Handlebars": "#f7931e",
|
| 242 |
+
"Harbour": "#0e60e3",
|
| 243 |
+
"Hare": "#9d7424",
|
| 244 |
+
"Haskell": "#5e5086",
|
| 245 |
+
"Haxe": "#df7900",
|
| 246 |
+
"HiveQL": "#dce200",
|
| 247 |
+
"HolyC": "#ffefaf",
|
| 248 |
+
"Hosts File": "#308888",
|
| 249 |
+
"Hurl": "#FF0288",
|
| 250 |
+
"Hy": "#7790B2",
|
| 251 |
+
"IDL": "#a3522f",
|
| 252 |
+
"IGOR Pro": "#0000cc",
|
| 253 |
+
"INI": "#d1dbe0",
|
| 254 |
+
"ISPC": "#2D68B1",
|
| 255 |
+
"Idris": "#b30000",
|
| 256 |
+
"Ignore List": "#000000",
|
| 257 |
+
"ImageJ Macro": "#99AAFF",
|
| 258 |
+
"Imba": "#16cec6",
|
| 259 |
+
"Inno Setup": "#264b99",
|
| 260 |
+
"Io": "#a9188d",
|
| 261 |
+
"Ioke": "#078193",
|
| 262 |
+
"Isabelle": "#FEFE00",
|
| 263 |
+
"Isabelle ROOT": "#FEFE00",
|
| 264 |
+
"J": "#9EEDFF",
|
| 265 |
+
"JAR Manifest": "#b07219",
|
| 266 |
+
"JCL": "#d90e09",
|
| 267 |
+
"JFlex": "#DBCA00",
|
| 268 |
+
"JSON": "#292929",
|
| 269 |
+
"JSON with Comments": "#292929",
|
| 270 |
+
"JSON5": "#267CB9",
|
| 271 |
+
"JSONLD": "#0c479c",
|
| 272 |
+
"JSONiq": "#40d47e",
|
| 273 |
+
"Jai": "#ab8b4b",
|
| 274 |
+
"Janet": "#0886a5",
|
| 275 |
+
"Jasmin": "#d03600",
|
| 276 |
+
"Java": "#b07219",
|
| 277 |
+
"Java Properties": "#2A6277",
|
| 278 |
+
"Java Server Pages": "#2A6277",
|
| 279 |
+
"Java Template Engine": "#2A6277",
|
| 280 |
+
"JavaScript": "#f1e05a",
|
| 281 |
+
"JavaScript+ERB": "#f1e05a",
|
| 282 |
+
"Jest Snapshot": "#15c213",
|
| 283 |
+
"JetBrains MPS": "#21D789",
|
| 284 |
+
"Jinja": "#a52a22",
|
| 285 |
+
"Jison": "#56b3cb",
|
| 286 |
+
"Jison Lex": "#56b3cb",
|
| 287 |
+
"Jolie": "#843179",
|
| 288 |
+
"Jsonnet": "#0064bd",
|
| 289 |
+
"Julia": "#a270ba",
|
| 290 |
+
"Julia REPL": "#a270ba",
|
| 291 |
+
"Jupyter Notebook": "#DA5B0B",
|
| 292 |
+
"Just": "#384d54",
|
| 293 |
+
"KDL": "#ffb3b3",
|
| 294 |
+
"KRL": "#28430A",
|
| 295 |
+
"Kaitai Struct": "#773b37",
|
| 296 |
+
"KakouneScript": "#6f8042",
|
| 297 |
+
"KerboScript": "#41adf0",
|
| 298 |
+
"KiCad Layout": "#2f4aab",
|
| 299 |
+
"KiCad Legacy Layout": "#2f4aab",
|
| 300 |
+
"KiCad Schematic": "#2f4aab",
|
| 301 |
+
"KoLmafia ASH": "#B9D9B9",
|
| 302 |
+
"Koka": "#215166",
|
| 303 |
+
"Kotlin": "#A97BFF",
|
| 304 |
+
"LFE": "#4C3023",
|
| 305 |
+
"LLVM": "#185619",
|
| 306 |
+
"LOLCODE": "#cc9900",
|
| 307 |
+
"LSL": "#3d9970",
|
| 308 |
+
"LabVIEW": "#fede06",
|
| 309 |
+
"Lark": "#2980B9",
|
| 310 |
+
"Lasso": "#999999",
|
| 311 |
+
"Latte": "#f2a542",
|
| 312 |
+
"Leo": "#C4FFC2",
|
| 313 |
+
"Less": "#1d365d",
|
| 314 |
+
"Lex": "#DBCA00",
|
| 315 |
+
"LigoLANG": "#0e74ff",
|
| 316 |
+
"LilyPond": "#9ccc7c",
|
| 317 |
+
"Liquid": "#67b8de",
|
| 318 |
+
"Literate Agda": "#315665",
|
| 319 |
+
"Literate CoffeeScript": "#244776",
|
| 320 |
+
"Literate Haskell": "#5e5086",
|
| 321 |
+
"LiveCode Script": "#0c5ba5",
|
| 322 |
+
"LiveScript": "#499886",
|
| 323 |
+
"Logtalk": "#295b9a",
|
| 324 |
+
"LookML": "#652B81",
|
| 325 |
+
"Lua": "#000080",
|
| 326 |
+
"Luau": "#00A2FF",
|
| 327 |
+
"M3U": "#179C7D",
|
| 328 |
+
"MATLAB": "#e16737",
|
| 329 |
+
"MAXScript": "#00a6a6",
|
| 330 |
+
"MDX": "#fcb32c",
|
| 331 |
+
"MLIR": "#5EC8DB",
|
| 332 |
+
"MQL4": "#62A8D6",
|
| 333 |
+
"MQL5": "#4A76B8",
|
| 334 |
+
"MTML": "#b7e1f4",
|
| 335 |
+
"Macaulay2": "#d8ffff",
|
| 336 |
+
"Makefile": "#427819",
|
| 337 |
+
"Mako": "#7e858d",
|
| 338 |
+
"Markdown": "#083fa1",
|
| 339 |
+
"Marko": "#42bff2",
|
| 340 |
+
"Mask": "#f97732",
|
| 341 |
+
"Max": "#c4a79c",
|
| 342 |
+
"Mercury": "#ff2b2b",
|
| 343 |
+
"Mermaid": "#ff3670",
|
| 344 |
+
"Meson": "#007800",
|
| 345 |
+
"Metal": "#8f14e9",
|
| 346 |
+
"MiniYAML": "#ff1111",
|
| 347 |
+
"MiniZinc": "#06a9e6",
|
| 348 |
+
"Mint": "#02b046",
|
| 349 |
+
"Mirah": "#c7a938",
|
| 350 |
+
"Modelica": "#de1d31",
|
| 351 |
+
"Modula-2": "#10253f",
|
| 352 |
+
"Modula-3": "#223388",
|
| 353 |
+
"Mojo": "#ff4c1f",
|
| 354 |
+
"Monkey C": "#8D6747",
|
| 355 |
+
"MoonBit": "#b92381",
|
| 356 |
+
"MoonScript": "#ff4585",
|
| 357 |
+
"Motoko": "#fbb03b",
|
| 358 |
+
"Motorola 68K Assembly": "#005daa",
|
| 359 |
+
"Move": "#4a137a",
|
| 360 |
+
"Mustache": "#724b3b",
|
| 361 |
+
"NCL": "#28431f",
|
| 362 |
+
"NMODL": "#00356B",
|
| 363 |
+
"NPM Config": "#cb3837",
|
| 364 |
+
"NWScript": "#111522",
|
| 365 |
+
"Nasal": "#1d2c4e",
|
| 366 |
+
"Nearley": "#990000",
|
| 367 |
+
"Nemerle": "#3d3c6e",
|
| 368 |
+
"NetLinx": "#0aa0ff",
|
| 369 |
+
"NetLinx+ERB": "#747faa",
|
| 370 |
+
"NetLogo": "#ff6375",
|
| 371 |
+
"NewLisp": "#87AED7",
|
| 372 |
+
"Nextflow": "#3ac486",
|
| 373 |
+
"Nginx": "#009639",
|
| 374 |
+
"Nickel": "#E0C3FC",
|
| 375 |
+
"Nim": "#ffc200",
|
| 376 |
+
"Nit": "#009917",
|
| 377 |
+
"Nix": "#7e7eff",
|
| 378 |
+
"Noir": "#2f1f49",
|
| 379 |
+
"Nu": "#c9df40",
|
| 380 |
+
"NumPy": "#9C8AF9",
|
| 381 |
+
"Nunjucks": "#3d8137",
|
| 382 |
+
"Nushell": "#4E9906",
|
| 383 |
+
"OASv2-json": "#85ea2d",
|
| 384 |
+
"OASv2-yaml": "#85ea2d",
|
| 385 |
+
"OASv3-json": "#85ea2d",
|
| 386 |
+
"OASv3-yaml": "#85ea2d",
|
| 387 |
+
"OCaml": "#ef7a08",
|
| 388 |
+
"OMNeT++ MSG": "#a0e0a0",
|
| 389 |
+
"OMNeT++ NED": "#08607c",
|
| 390 |
+
"ObjectScript": "#424893",
|
| 391 |
+
"Objective-C": "#438eff",
|
| 392 |
+
"Objective-C++": "#6866fb",
|
| 393 |
+
"Objective-J": "#ff0c5a",
|
| 394 |
+
"Odin": "#60AFFE",
|
| 395 |
+
"Omgrofl": "#cabbff",
|
| 396 |
+
"Opal": "#f7ede0",
|
| 397 |
+
"Open Policy Agent": "#7d9199",
|
| 398 |
+
"OpenAPI Specification v2": "#85ea2d",
|
| 399 |
+
"OpenAPI Specification v3": "#85ea2d",
|
| 400 |
+
"OpenCL": "#ed2e2d",
|
| 401 |
+
"OpenEdge ABL": "#5ce600",
|
| 402 |
+
"OpenQASM": "#AA70FF",
|
| 403 |
+
"OpenSCAD": "#e5cd45",
|
| 404 |
+
"Option List": "#476732",
|
| 405 |
+
"Org": "#77aa99",
|
| 406 |
+
"OverpassQL": "#cce2aa",
|
| 407 |
+
"Oxygene": "#cdd0e3",
|
| 408 |
+
"Oz": "#fab738",
|
| 409 |
+
"P4": "#7055b5",
|
| 410 |
+
"PDDL": "#0d00ff",
|
| 411 |
+
"PEG.js": "#234d6b",
|
| 412 |
+
"PHP": "#4F5D95",
|
| 413 |
+
"PLSQL": "#dad8d8",
|
| 414 |
+
"PLpgSQL": "#336790",
|
| 415 |
+
"POV-Ray SDL": "#6bac65",
|
| 416 |
+
"Pact": "#F7A8B8",
|
| 417 |
+
"Pan": "#cc0000",
|
| 418 |
+
"Papyrus": "#6600cc",
|
| 419 |
+
"Parrot": "#f3ca0a",
|
| 420 |
+
"Pascal": "#E3F171",
|
| 421 |
+
"Pawn": "#dbb284",
|
| 422 |
+
"Pep8": "#C76F5B",
|
| 423 |
+
"Perl": "#0298c3",
|
| 424 |
+
"PicoLisp": "#6067af",
|
| 425 |
+
"PigLatin": "#fcd7de",
|
| 426 |
+
"Pike": "#005390",
|
| 427 |
+
"Pip Requirements": "#FFD343",
|
| 428 |
+
"Pkl": "#6b9543",
|
| 429 |
+
"PlantUML": "#fbbd16",
|
| 430 |
+
"PogoScript": "#d80074",
|
| 431 |
+
"Polar": "#ae81ff",
|
| 432 |
+
"Portugol": "#f8bd00",
|
| 433 |
+
"PostCSS": "#dc3a0c",
|
| 434 |
+
"PostScript": "#da291c",
|
| 435 |
+
"PowerBuilder": "#8f0f8d",
|
| 436 |
+
"PowerShell": "#012456",
|
| 437 |
+
"Praat": "#c8506d",
|
| 438 |
+
"Prisma": "#0c344b",
|
| 439 |
+
"Processing": "#0096D8",
|
| 440 |
+
"Procfile": "#3B2F63",
|
| 441 |
+
"Prolog": "#74283c",
|
| 442 |
+
"Promela": "#de0000",
|
| 443 |
+
"Propeller Spin": "#7fa2a7",
|
| 444 |
+
"Pug": "#a86454",
|
| 445 |
+
"Puppet": "#302B6D",
|
| 446 |
+
"PureBasic": "#5a6986",
|
| 447 |
+
"PureScript": "#1D222D",
|
| 448 |
+
"Pyret": "#ee1e10",
|
| 449 |
+
"Python": "#3572A5",
|
| 450 |
+
"Python console": "#3572A5",
|
| 451 |
+
"Python traceback": "#3572A5",
|
| 452 |
+
"Q#": "#fed659",
|
| 453 |
+
"QML": "#44a51c",
|
| 454 |
+
"Qt Script": "#00b841",
|
| 455 |
+
"Quake": "#882233",
|
| 456 |
+
"QuakeC": "#975777",
|
| 457 |
+
"QuickBASIC": "#008080",
|
| 458 |
+
"R": "#198CE7",
|
| 459 |
+
"RAML": "#77d9fb",
|
| 460 |
+
"RBS": "#701516",
|
| 461 |
+
"RDoc": "#701516",
|
| 462 |
+
"REXX": "#d90e09",
|
| 463 |
+
"RMarkdown": "#198ce7",
|
| 464 |
+
"RON": "#a62c00",
|
| 465 |
+
"ROS Interface": "#22314e",
|
| 466 |
+
"RPGLE": "#2BDE21",
|
| 467 |
+
"RUNOFF": "#665a4e",
|
| 468 |
+
"Racket": "#3c5caa",
|
| 469 |
+
"Ragel": "#9d5200",
|
| 470 |
+
"Raku": "#0000fb",
|
| 471 |
+
"Rascal": "#fffaa0",
|
| 472 |
+
"ReScript": "#ed5051",
|
| 473 |
+
"Reason": "#ff5847",
|
| 474 |
+
"ReasonLIGO": "#ff5847",
|
| 475 |
+
"Rebol": "#358a5b",
|
| 476 |
+
"Record Jar": "#0673ba",
|
| 477 |
+
"Red": "#f50000",
|
| 478 |
+
"Regular Expression": "#009a00",
|
| 479 |
+
"Ren'Py": "#ff7f7f",
|
| 480 |
+
"Rez": "#FFDAB3",
|
| 481 |
+
"Ring": "#2D54CB",
|
| 482 |
+
"Riot": "#A71E49",
|
| 483 |
+
"RobotFramework": "#00c0b5",
|
| 484 |
+
"Roc": "#7c38f5",
|
| 485 |
+
"Rocq Prover": "#d0b68c",
|
| 486 |
+
"Roff": "#ecdebe",
|
| 487 |
+
"Roff Manpage": "#ecdebe",
|
| 488 |
+
"Rouge": "#cc0088",
|
| 489 |
+
"RouterOS Script": "#DE3941",
|
| 490 |
+
"Ruby": "#701516",
|
| 491 |
+
"Rust": "#dea584",
|
| 492 |
+
"SAS": "#B34936",
|
| 493 |
+
"SCSS": "#c6538c",
|
| 494 |
+
"SPARQL": "#0C4597",
|
| 495 |
+
"SQF": "#3F3F3F",
|
| 496 |
+
"SQL": "#e38c00",
|
| 497 |
+
"SQLPL": "#e38c00",
|
| 498 |
+
"SRecode Template": "#348a34",
|
| 499 |
+
"STL": "#373b5e",
|
| 500 |
+
"SVG": "#ff9900",
|
| 501 |
+
"Sail": "#259dd5",
|
| 502 |
+
"SaltStack": "#646464",
|
| 503 |
+
"Sass": "#a53b70",
|
| 504 |
+
"Scala": "#c22d40",
|
| 505 |
+
"Scaml": "#bd181a",
|
| 506 |
+
"Scenic": "#fdc700",
|
| 507 |
+
"Scheme": "#1e4aec",
|
| 508 |
+
"Scilab": "#ca0f21",
|
| 509 |
+
"Self": "#0579aa",
|
| 510 |
+
"ShaderLab": "#222c37",
|
| 511 |
+
"Shell": "#89e051",
|
| 512 |
+
"ShellCheck Config": "#cecfcb",
|
| 513 |
+
"Shen": "#120F14",
|
| 514 |
+
"Simple File Verification": "#C9BFED",
|
| 515 |
+
"Singularity": "#64E6AD",
|
| 516 |
+
"Slang": "#1fbec9",
|
| 517 |
+
"Slash": "#007eff",
|
| 518 |
+
"Slice": "#003fa2",
|
| 519 |
+
"Slim": "#2b2b2b",
|
| 520 |
+
"Slint": "#2379F4",
|
| 521 |
+
"SmPL": "#c94949",
|
| 522 |
+
"Smalltalk": "#596706",
|
| 523 |
+
"Smarty": "#f0c040",
|
| 524 |
+
"Smithy": "#c44536",
|
| 525 |
+
"Snakemake": "#419179",
|
| 526 |
+
"Solidity": "#AA6746",
|
| 527 |
+
"SourcePawn": "#f69e1d",
|
| 528 |
+
"Squirrel": "#800000",
|
| 529 |
+
"Stan": "#b2011d",
|
| 530 |
+
"Standard ML": "#dc566d",
|
| 531 |
+
"Starlark": "#76d275",
|
| 532 |
+
"Stata": "#1a5f91",
|
| 533 |
+
"StringTemplate": "#3fb34f",
|
| 534 |
+
"Stylus": "#ff6347",
|
| 535 |
+
"SubRip Text": "#9e0101",
|
| 536 |
+
"SugarSS": "#2fcc9f",
|
| 537 |
+
"SuperCollider": "#46390b",
|
| 538 |
+
"Survex data": "#ffcc99",
|
| 539 |
+
"Svelte": "#ff3e00",
|
| 540 |
+
"Sway": "#00F58C",
|
| 541 |
+
"Sweave": "#198ce7",
|
| 542 |
+
"Swift": "#F05138",
|
| 543 |
+
"SystemVerilog": "#DAE1C2",
|
| 544 |
+
"TI Program": "#A0AA87",
|
| 545 |
+
"TL-Verilog": "#C40023",
|
| 546 |
+
"TLA": "#4b0079",
|
| 547 |
+
"TOML": "#9c4221",
|
| 548 |
+
"TSQL": "#e38c00",
|
| 549 |
+
"TSV": "#237346",
|
| 550 |
+
"TSX": "#3178c6",
|
| 551 |
+
"TXL": "#0178b8",
|
| 552 |
+
"Tact": "#48b5ff",
|
| 553 |
+
"Talon": "#333333",
|
| 554 |
+
"Tcl": "#e4cc98",
|
| 555 |
+
"TeX": "#3D6117",
|
| 556 |
+
"Teal": "#00B1BC",
|
| 557 |
+
"Terra": "#00004c",
|
| 558 |
+
"Terraform Template": "#7b42bb",
|
| 559 |
+
"TextGrid": "#c8506d",
|
| 560 |
+
"TextMate Properties": "#df66e4",
|
| 561 |
+
"Textile": "#ffe7ac",
|
| 562 |
+
"Thrift": "#D12127",
|
| 563 |
+
"Toit": "#c2c9fb",
|
| 564 |
+
"Tor Config": "#59316b",
|
| 565 |
+
"Tree-sitter Query": "#8ea64c",
|
| 566 |
+
"Turing": "#cf142b",
|
| 567 |
+
"Twig": "#c1d026",
|
| 568 |
+
"TypeScript": "#3178c6",
|
| 569 |
+
"TypeSpec": "#4A3665",
|
| 570 |
+
"Typst": "#239dad",
|
| 571 |
+
"Unified Parallel C": "#4e3617",
|
| 572 |
+
"Unity3D Asset": "#222c37",
|
| 573 |
+
"Uno": "#9933cc",
|
| 574 |
+
"UnrealScript": "#a54c4d",
|
| 575 |
+
"Untyped Plutus Core": "#36adbd",
|
| 576 |
+
"UrWeb": "#ccccee",
|
| 577 |
+
"V": "#4f87c4",
|
| 578 |
+
"VBA": "#867db1",
|
| 579 |
+
"VBScript": "#15dcdc",
|
| 580 |
+
"VCL": "#148AA8",
|
| 581 |
+
"VHDL": "#adb2cb",
|
| 582 |
+
"Vala": "#a56de2",
|
| 583 |
+
"Valve Data Format": "#f26025",
|
| 584 |
+
"Velocity Template Language": "#507cff",
|
| 585 |
+
"Vento": "#ff0080",
|
| 586 |
+
"Verilog": "#b2b7f8",
|
| 587 |
+
"Vim Help File": "#199f4b",
|
| 588 |
+
"Vim Script": "#199f4b",
|
| 589 |
+
"Vim Snippet": "#199f4b",
|
| 590 |
+
"Visual Basic .NET": "#945db7",
|
| 591 |
+
"Visual Basic 6.0": "#2c6353",
|
| 592 |
+
"Volt": "#1F1F1F",
|
| 593 |
+
"Vue": "#41b883",
|
| 594 |
+
"Vyper": "#9F4CF2",
|
| 595 |
+
"WDL": "#42f1f4",
|
| 596 |
+
"WGSL": "#1a5e9a",
|
| 597 |
+
"Web Ontology Language": "#5b70bd",
|
| 598 |
+
"WebAssembly": "#04133b",
|
| 599 |
+
"WebAssembly Interface Type": "#6250e7",
|
| 600 |
+
"Whiley": "#d5c397",
|
| 601 |
+
"Wikitext": "#fc5757",
|
| 602 |
+
"Windows Registry Entries": "#52d5ff",
|
| 603 |
+
"Witcher Script": "#ff0000",
|
| 604 |
+
"Wolfram Language": "#dd1100",
|
| 605 |
+
"Wollok": "#a23738",
|
| 606 |
+
"World of Warcraft Addon Data": "#f7e43f",
|
| 607 |
+
"Wren": "#383838",
|
| 608 |
+
"X10": "#4B6BEF",
|
| 609 |
+
"XC": "#99DA07",
|
| 610 |
+
"XML": "#0060ac",
|
| 611 |
+
"XML Property List": "#0060ac",
|
| 612 |
+
"XQuery": "#5232e7",
|
| 613 |
+
"XSLT": "#EB8CEB",
|
| 614 |
+
"Xmake": "#22a079",
|
| 615 |
+
"Xojo": "#81bd41",
|
| 616 |
+
"Xonsh": "#285EEF",
|
| 617 |
+
"Xtend": "#24255d",
|
| 618 |
+
"YAML": "#cb171e",
|
| 619 |
+
"YARA": "#220000",
|
| 620 |
+
"YASnippet": "#32AB90",
|
| 621 |
+
"Yacc": "#4B6C4B",
|
| 622 |
+
"Yul": "#794932",
|
| 623 |
+
"ZAP": "#0d665e",
|
| 624 |
+
"ZIL": "#dc75e5",
|
| 625 |
+
"ZenScript": "#00BCD1",
|
| 626 |
+
"Zephir": "#118f9e",
|
| 627 |
+
"Zig": "#ec915c",
|
| 628 |
+
"Zimpl": "#d67711",
|
| 629 |
+
"Zmodel": "#ff7100",
|
| 630 |
+
"crontab": "#ead7ac",
|
| 631 |
+
"eC": "#913960",
|
| 632 |
+
"fish": "#4aae47",
|
| 633 |
+
"hoon": "#00b171",
|
| 634 |
+
"iCalendar": "#ec564c",
|
| 635 |
+
"jq": "#c7254e",
|
| 636 |
+
"kvlang": "#1da6e0",
|
| 637 |
+
"mIRC Script": "#3d57c3",
|
| 638 |
+
"mcfunction": "#E22837",
|
| 639 |
+
"mdsvex": "#5f9ea0",
|
| 640 |
+
"mupad": "#244963",
|
| 641 |
+
"nanorc": "#2d004d",
|
| 642 |
+
"nesC": "#94B0C7",
|
| 643 |
+
"ooc": "#b0b77e",
|
| 644 |
+
"q": "#0040cd",
|
| 645 |
+
"reStructuredText": "#141414",
|
| 646 |
+
"sed": "#64b970",
|
| 647 |
+
"templ": "#66D0DD",
|
| 648 |
+
"vCard": "#ee2647",
|
| 649 |
+
"wisp": "#7582D1",
|
| 650 |
+
"xBase": "#403a40"
|
| 651 |
+
}
|
src/common/log.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
const noop = () => {};
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Return console instance based on the environment.
|
| 7 |
+
*
|
| 8 |
+
* @type {Console | {log: () => void, error: () => void}}
|
| 9 |
+
*/
|
| 10 |
+
const logger =
|
| 11 |
+
process.env.NODE_ENV === "test" ? { log: noop, error: noop } : console;
|
| 12 |
+
|
| 13 |
+
export { logger };
|
| 14 |
+
export default logger;
|
src/common/ops.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import toEmoji from "emoji-name-map";
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Returns boolean if value is either "true" or "false" else the value as it is.
|
| 7 |
+
*
|
| 8 |
+
* @param {string | boolean} value The value to parse.
|
| 9 |
+
* @returns {boolean | undefined } The parsed value.
|
| 10 |
+
*/
|
| 11 |
+
const parseBoolean = (value) => {
|
| 12 |
+
if (typeof value === "boolean") {
|
| 13 |
+
return value;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
if (typeof value === "string") {
|
| 17 |
+
if (value.toLowerCase() === "true") {
|
| 18 |
+
return true;
|
| 19 |
+
} else if (value.toLowerCase() === "false") {
|
| 20 |
+
return false;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
return undefined;
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Parse string to array of strings.
|
| 28 |
+
*
|
| 29 |
+
* @param {string} str The string to parse.
|
| 30 |
+
* @returns {string[]} The array of strings.
|
| 31 |
+
*/
|
| 32 |
+
const parseArray = (str) => {
|
| 33 |
+
if (!str) {
|
| 34 |
+
return [];
|
| 35 |
+
}
|
| 36 |
+
return str.split(",");
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Clamp the given number between the given range.
|
| 41 |
+
*
|
| 42 |
+
* @param {number} number The number to clamp.
|
| 43 |
+
* @param {number} min The minimum value.
|
| 44 |
+
* @param {number} max The maximum value.
|
| 45 |
+
* @returns {number} The clamped number.
|
| 46 |
+
*/
|
| 47 |
+
const clampValue = (number, min, max) => {
|
| 48 |
+
// @ts-ignore
|
| 49 |
+
if (Number.isNaN(parseInt(number, 10))) {
|
| 50 |
+
return min;
|
| 51 |
+
}
|
| 52 |
+
return Math.max(min, Math.min(number, max));
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Lowercase and trim string.
|
| 57 |
+
*
|
| 58 |
+
* @param {string} name String to lowercase and trim.
|
| 59 |
+
* @returns {string} Lowercased and trimmed string.
|
| 60 |
+
*/
|
| 61 |
+
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Split array of languages in two columns.
|
| 65 |
+
*
|
| 66 |
+
* @template T Language object.
|
| 67 |
+
* @param {Array<T>} arr Array of languages.
|
| 68 |
+
* @param {number} perChunk Number of languages per column.
|
| 69 |
+
* @returns {Array<T>} Array of languages split in two columns.
|
| 70 |
+
*/
|
| 71 |
+
const chunkArray = (arr, perChunk) => {
|
| 72 |
+
return arr.reduce((resultArray, item, index) => {
|
| 73 |
+
const chunkIndex = Math.floor(index / perChunk);
|
| 74 |
+
|
| 75 |
+
if (!resultArray[chunkIndex]) {
|
| 76 |
+
// @ts-ignore
|
| 77 |
+
resultArray[chunkIndex] = []; // start a new chunk
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// @ts-ignore
|
| 81 |
+
resultArray[chunkIndex].push(item);
|
| 82 |
+
|
| 83 |
+
return resultArray;
|
| 84 |
+
}, []);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Parse emoji from string.
|
| 89 |
+
*
|
| 90 |
+
* @param {string} str String to parse emoji from.
|
| 91 |
+
* @returns {string} String with emoji parsed.
|
| 92 |
+
*/
|
| 93 |
+
const parseEmojis = (str) => {
|
| 94 |
+
if (!str) {
|
| 95 |
+
throw new Error("[parseEmoji]: str argument not provided");
|
| 96 |
+
}
|
| 97 |
+
return str.replace(/:\w+:/gm, (emoji) => {
|
| 98 |
+
return toEmoji.get(emoji) || "";
|
| 99 |
+
});
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Get diff in minutes between two dates.
|
| 104 |
+
*
|
| 105 |
+
* @param {Date} d1 First date.
|
| 106 |
+
* @param {Date} d2 Second date.
|
| 107 |
+
* @returns {number} Number of minutes between the two dates.
|
| 108 |
+
*/
|
| 109 |
+
const dateDiff = (d1, d2) => {
|
| 110 |
+
const date1 = new Date(d1);
|
| 111 |
+
const date2 = new Date(d2);
|
| 112 |
+
const diff = date1.getTime() - date2.getTime();
|
| 113 |
+
return Math.round(diff / (1000 * 60));
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
export {
|
| 117 |
+
parseBoolean,
|
| 118 |
+
parseArray,
|
| 119 |
+
clampValue,
|
| 120 |
+
lowercaseTrim,
|
| 121 |
+
chunkArray,
|
| 122 |
+
parseEmojis,
|
| 123 |
+
dateDiff,
|
| 124 |
+
};
|
src/common/render.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
|
| 4 |
+
import { getCardColors } from "./color.js";
|
| 5 |
+
import { encodeHTML } from "./html.js";
|
| 6 |
+
import { clampValue } from "./ops.js";
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Auto layout utility, allows us to layout things vertically or horizontally with
|
| 10 |
+
* proper gaping.
|
| 11 |
+
*
|
| 12 |
+
* @param {object} props Function properties.
|
| 13 |
+
* @param {string[]} props.items Array of items to layout.
|
| 14 |
+
* @param {number} props.gap Gap between items.
|
| 15 |
+
* @param {"column" | "row"=} props.direction Direction to layout items.
|
| 16 |
+
* @param {number[]=} props.sizes Array of sizes for each item.
|
| 17 |
+
* @returns {string[]} Array of items with proper layout.
|
| 18 |
+
*/
|
| 19 |
+
const flexLayout = ({ items, gap, direction, sizes = [] }) => {
|
| 20 |
+
let lastSize = 0;
|
| 21 |
+
// filter() for filtering out empty strings
|
| 22 |
+
return items.filter(Boolean).map((item, i) => {
|
| 23 |
+
const size = sizes[i] || 0;
|
| 24 |
+
let transform = `translate(${lastSize}, 0)`;
|
| 25 |
+
if (direction === "column") {
|
| 26 |
+
transform = `translate(0, ${lastSize})`;
|
| 27 |
+
}
|
| 28 |
+
lastSize += size + gap;
|
| 29 |
+
return `<g transform="${transform}">${item}</g>`;
|
| 30 |
+
});
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Creates a node to display the primary programming language of the repository/gist.
|
| 35 |
+
*
|
| 36 |
+
* @param {string} langName Language name.
|
| 37 |
+
* @param {string} langColor Language color.
|
| 38 |
+
* @returns {string} Language display SVG object.
|
| 39 |
+
*/
|
| 40 |
+
const createLanguageNode = (langName, langColor) => {
|
| 41 |
+
return `
|
| 42 |
+
<g data-testid="primary-lang">
|
| 43 |
+
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
| 44 |
+
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
| 45 |
+
</g>
|
| 46 |
+
`;
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Create a node to indicate progress in percentage along a horizontal line.
|
| 51 |
+
*
|
| 52 |
+
* @param {Object} params Object that contains the createProgressNode parameters.
|
| 53 |
+
* @param {number} params.x X-axis position.
|
| 54 |
+
* @param {number} params.y Y-axis position.
|
| 55 |
+
* @param {number} params.width Width of progress bar.
|
| 56 |
+
* @param {string} params.color Progress color.
|
| 57 |
+
* @param {number} params.progress Progress value.
|
| 58 |
+
* @param {string} params.progressBarBackgroundColor Progress bar bg color.
|
| 59 |
+
* @param {number} params.delay Delay before animation starts.
|
| 60 |
+
* @returns {string} Progress node.
|
| 61 |
+
*/
|
| 62 |
+
const createProgressNode = ({
|
| 63 |
+
x,
|
| 64 |
+
y,
|
| 65 |
+
width,
|
| 66 |
+
color,
|
| 67 |
+
progress,
|
| 68 |
+
progressBarBackgroundColor,
|
| 69 |
+
delay,
|
| 70 |
+
}) => {
|
| 71 |
+
const progressPercentage = clampValue(progress, 2, 100);
|
| 72 |
+
|
| 73 |
+
return `
|
| 74 |
+
<svg width="${width}" x="${x}" y="${y}">
|
| 75 |
+
<rect rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
| 76 |
+
<svg data-testid="lang-progress" width="${progressPercentage}%">
|
| 77 |
+
<rect
|
| 78 |
+
height="8"
|
| 79 |
+
fill="${color}"
|
| 80 |
+
rx="5" ry="5" x="0" y="0"
|
| 81 |
+
class="lang-progress"
|
| 82 |
+
style="animation-delay: ${delay}ms;"
|
| 83 |
+
/>
|
| 84 |
+
</svg>
|
| 85 |
+
</svg>
|
| 86 |
+
`;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
|
| 91 |
+
*
|
| 92 |
+
* @param {string} icon The icon to display.
|
| 93 |
+
* @param {number|string} label The label to display.
|
| 94 |
+
* @param {string} testid The testid to assign to the label.
|
| 95 |
+
* @param {number} iconSize The size of the icon.
|
| 96 |
+
* @returns {string} Icon with label SVG object.
|
| 97 |
+
*/
|
| 98 |
+
const iconWithLabel = (icon, label, testid, iconSize) => {
|
| 99 |
+
if (typeof label === "number" && label <= 0) {
|
| 100 |
+
return "";
|
| 101 |
+
}
|
| 102 |
+
const iconSvg = `
|
| 103 |
+
<svg
|
| 104 |
+
class="icon"
|
| 105 |
+
y="-12"
|
| 106 |
+
viewBox="0 0 16 16"
|
| 107 |
+
version="1.1"
|
| 108 |
+
width="${iconSize}"
|
| 109 |
+
height="${iconSize}"
|
| 110 |
+
>
|
| 111 |
+
${icon}
|
| 112 |
+
</svg>
|
| 113 |
+
`;
|
| 114 |
+
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
|
| 115 |
+
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
// Script parameters.
|
| 119 |
+
const ERROR_CARD_LENGTH = 576.5;
|
| 120 |
+
|
| 121 |
+
const UPSTREAM_API_ERRORS = [
|
| 122 |
+
TRY_AGAIN_LATER,
|
| 123 |
+
SECONDARY_ERROR_MESSAGES.MAX_RETRY,
|
| 124 |
+
];
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* Renders error message on the card.
|
| 128 |
+
*
|
| 129 |
+
* @param {object} args Function arguments.
|
| 130 |
+
* @param {string} args.message Main error message.
|
| 131 |
+
* @param {string} [args.secondaryMessage=""] The secondary error message.
|
| 132 |
+
* @param {object} [args.renderOptions={}] Render options.
|
| 133 |
+
* @param {string=} args.renderOptions.title_color Card title color.
|
| 134 |
+
* @param {string=} args.renderOptions.text_color Card text color.
|
| 135 |
+
* @param {string=} args.renderOptions.bg_color Card background color.
|
| 136 |
+
* @param {string=} args.renderOptions.border_color Card border color.
|
| 137 |
+
* @param {Parameters<typeof getCardColors>[0]["theme"]=} args.renderOptions.theme Card theme.
|
| 138 |
+
* @param {boolean=} args.renderOptions.show_repo_link Whether to show repo link or not.
|
| 139 |
+
* @returns {string} The SVG markup.
|
| 140 |
+
*/
|
| 141 |
+
const renderError = ({
|
| 142 |
+
message,
|
| 143 |
+
secondaryMessage = "",
|
| 144 |
+
renderOptions = {},
|
| 145 |
+
}) => {
|
| 146 |
+
const {
|
| 147 |
+
title_color,
|
| 148 |
+
text_color,
|
| 149 |
+
bg_color,
|
| 150 |
+
border_color,
|
| 151 |
+
theme = "default",
|
| 152 |
+
show_repo_link = true,
|
| 153 |
+
} = renderOptions;
|
| 154 |
+
|
| 155 |
+
// returns theme based colors with proper overrides and defaults
|
| 156 |
+
const { titleColor, textColor, bgColor, borderColor } = getCardColors({
|
| 157 |
+
title_color,
|
| 158 |
+
text_color,
|
| 159 |
+
icon_color: "",
|
| 160 |
+
bg_color,
|
| 161 |
+
border_color,
|
| 162 |
+
ring_color: "",
|
| 163 |
+
theme,
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
return `
|
| 167 |
+
<svg width="${ERROR_CARD_LENGTH}" height="120" viewBox="0 0 ${ERROR_CARD_LENGTH} 120" fill="${bgColor}" xmlns="http://www.w3.org/2000/svg">
|
| 168 |
+
<style>
|
| 169 |
+
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
| 170 |
+
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
| 171 |
+
.gray { fill: #858585 }
|
| 172 |
+
</style>
|
| 173 |
+
<rect x="0.5" y="0.5" width="${
|
| 174 |
+
ERROR_CARD_LENGTH - 1
|
| 175 |
+
}" height="99%" rx="4.5" fill="${bgColor}" stroke="${borderColor}"/>
|
| 176 |
+
<text x="25" y="45" class="text">Something went wrong!${
|
| 177 |
+
UPSTREAM_API_ERRORS.includes(secondaryMessage) || !show_repo_link
|
| 178 |
+
? ""
|
| 179 |
+
: " file an issue at https://tiny.one/readme-stats"
|
| 180 |
+
}</text>
|
| 181 |
+
<text data-testid="message" x="25" y="55" class="text small">
|
| 182 |
+
<tspan x="25" dy="18">${encodeHTML(message)}</tspan>
|
| 183 |
+
<tspan x="25" dy="18" class="gray">${secondaryMessage}</tspan>
|
| 184 |
+
</text>
|
| 185 |
+
</svg>
|
| 186 |
+
`;
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* Retrieve text length.
|
| 191 |
+
*
|
| 192 |
+
* @see https://stackoverflow.com/a/48172630/10629172
|
| 193 |
+
* @param {string} str String to measure.
|
| 194 |
+
* @param {number} fontSize Font size.
|
| 195 |
+
* @returns {number} Text length.
|
| 196 |
+
*/
|
| 197 |
+
const measureText = (str, fontSize = 10) => {
|
| 198 |
+
// prettier-ignore
|
| 199 |
+
const widths = [
|
| 200 |
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
| 201 |
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
| 202 |
+
0, 0, 0, 0, 0.2796875, 0.2765625,
|
| 203 |
+
0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
|
| 204 |
+
0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
|
| 205 |
+
0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
| 206 |
+
0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
| 207 |
+
0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
|
| 208 |
+
1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
|
| 209 |
+
0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
|
| 210 |
+
0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
|
| 211 |
+
0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
|
| 212 |
+
0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
|
| 213 |
+
0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
|
| 214 |
+
0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
|
| 215 |
+
0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
|
| 216 |
+
0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
|
| 217 |
+
0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
|
| 218 |
+
];
|
| 219 |
+
|
| 220 |
+
const avg = 0.5279276315789471;
|
| 221 |
+
return (
|
| 222 |
+
str
|
| 223 |
+
.split("")
|
| 224 |
+
.map((c) =>
|
| 225 |
+
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
|
| 226 |
+
)
|
| 227 |
+
.reduce((cur, acc) => acc + cur) * fontSize
|
| 228 |
+
);
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
export {
|
| 232 |
+
ERROR_CARD_LENGTH,
|
| 233 |
+
renderError,
|
| 234 |
+
createLanguageNode,
|
| 235 |
+
createProgressNode,
|
| 236 |
+
iconWithLabel,
|
| 237 |
+
flexLayout,
|
| 238 |
+
measureText,
|
| 239 |
+
};
|
src/common/retryer.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { CustomError } from "./error.js";
|
| 4 |
+
import { logger } from "./log.js";
|
| 5 |
+
|
| 6 |
+
// Script variables.
|
| 7 |
+
|
| 8 |
+
// Count the number of GitHub API tokens available.
|
| 9 |
+
const PATs = Object.keys(process.env).filter((key) =>
|
| 10 |
+
/PAT_\d*$/.exec(key),
|
| 11 |
+
).length;
|
| 12 |
+
const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs;
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
|
| 16 |
+
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
|
| 17 |
+
*/
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
|
| 21 |
+
*
|
| 22 |
+
* @param {FetcherFunction} fetcher The fetcher function.
|
| 23 |
+
* @param {any} variables Object with arguments to pass to the fetcher function.
|
| 24 |
+
* @param {number} retries How many times to retry.
|
| 25 |
+
* @returns {Promise<any>} The response from the fetcher function.
|
| 26 |
+
*/
|
| 27 |
+
const retryer = async (fetcher, variables, retries = 0) => {
|
| 28 |
+
if (!RETRIES) {
|
| 29 |
+
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
if (retries > RETRIES) {
|
| 33 |
+
throw new CustomError(
|
| 34 |
+
"Downtime due to GitHub API rate limiting",
|
| 35 |
+
CustomError.MAX_RETRY,
|
| 36 |
+
);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
|
| 41 |
+
let response = await fetcher(
|
| 42 |
+
variables,
|
| 43 |
+
// @ts-ignore
|
| 44 |
+
process.env[`PAT_${retries + 1}`],
|
| 45 |
+
// used in tests for faking rate limit
|
| 46 |
+
retries,
|
| 47 |
+
);
|
| 48 |
+
|
| 49 |
+
// react on both type and message-based rate-limit signals.
|
| 50 |
+
// https://github.com/anuraghazra/github-readme-stats/issues/4425
|
| 51 |
+
const errors = response?.data?.errors;
|
| 52 |
+
const errorType = errors?.[0]?.type;
|
| 53 |
+
const errorMsg = errors?.[0]?.message || "";
|
| 54 |
+
const isRateLimited =
|
| 55 |
+
(errors && errorType === "RATE_LIMITED") || /rate limit/i.test(errorMsg);
|
| 56 |
+
|
| 57 |
+
// if rate limit is hit increase the RETRIES and recursively call the retryer
|
| 58 |
+
// with username, and current RETRIES
|
| 59 |
+
if (isRateLimited) {
|
| 60 |
+
logger.log(`PAT_${retries + 1} Failed`);
|
| 61 |
+
retries++;
|
| 62 |
+
// directly return from the function
|
| 63 |
+
return retryer(fetcher, variables, retries);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// finally return the response
|
| 67 |
+
return response;
|
| 68 |
+
} catch (err) {
|
| 69 |
+
/** @type {any} */
|
| 70 |
+
const e = err;
|
| 71 |
+
|
| 72 |
+
// network/unexpected error → let caller treat as failure
|
| 73 |
+
if (!e?.response) {
|
| 74 |
+
throw e;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// prettier-ignore
|
| 78 |
+
// also checking for bad credentials if any tokens gets invalidated
|
| 79 |
+
const isBadCredential =
|
| 80 |
+
e?.response?.data?.message === "Bad credentials";
|
| 81 |
+
const isAccountSuspended =
|
| 82 |
+
e?.response?.data?.message === "Sorry. Your account was suspended.";
|
| 83 |
+
|
| 84 |
+
if (isBadCredential || isAccountSuspended) {
|
| 85 |
+
logger.log(`PAT_${retries + 1} Failed`);
|
| 86 |
+
retries++;
|
| 87 |
+
// directly return from the function
|
| 88 |
+
return retryer(fetcher, variables, retries);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// HTTP error with a response → return it for caller-side handling
|
| 92 |
+
return e.response;
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
export { retryer, RETRIES };
|
| 97 |
+
export default retryer;
|
src/fetchers/gist.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { retryer } from "../common/retryer.js";
|
| 4 |
+
import { MissingParamError } from "../common/error.js";
|
| 5 |
+
import { request } from "../common/http.js";
|
| 6 |
+
|
| 7 |
+
const QUERY = `
|
| 8 |
+
query gistInfo($gistName: String!) {
|
| 9 |
+
viewer {
|
| 10 |
+
gist(name: $gistName) {
|
| 11 |
+
description
|
| 12 |
+
owner {
|
| 13 |
+
login
|
| 14 |
+
}
|
| 15 |
+
stargazerCount
|
| 16 |
+
forks {
|
| 17 |
+
totalCount
|
| 18 |
+
}
|
| 19 |
+
files {
|
| 20 |
+
name
|
| 21 |
+
language {
|
| 22 |
+
name
|
| 23 |
+
}
|
| 24 |
+
size
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
`;
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Gist data fetcher.
|
| 33 |
+
*
|
| 34 |
+
* @param {object} variables Fetcher variables.
|
| 35 |
+
* @param {string} token GitHub token.
|
| 36 |
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
| 37 |
+
*/
|
| 38 |
+
const fetcher = async (variables, token) => {
|
| 39 |
+
return await request(
|
| 40 |
+
{ query: QUERY, variables },
|
| 41 |
+
{ Authorization: `token ${token}` },
|
| 42 |
+
);
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* @typedef {{ name: string; language: { name: string; }, size: number }} GistFile Gist file.
|
| 47 |
+
*/
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* This function calculates the primary language of a gist by files size.
|
| 51 |
+
*
|
| 52 |
+
* @param {GistFile[]} files Files.
|
| 53 |
+
* @returns {string} Primary language.
|
| 54 |
+
*/
|
| 55 |
+
const calculatePrimaryLanguage = (files) => {
|
| 56 |
+
/** @type {Record<string, number>} */
|
| 57 |
+
const languages = {};
|
| 58 |
+
|
| 59 |
+
for (const file of files) {
|
| 60 |
+
if (file.language) {
|
| 61 |
+
if (languages[file.language.name]) {
|
| 62 |
+
languages[file.language.name] += file.size;
|
| 63 |
+
} else {
|
| 64 |
+
languages[file.language.name] = file.size;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
let primaryLanguage = Object.keys(languages)[0];
|
| 70 |
+
for (const language in languages) {
|
| 71 |
+
if (languages[language] > languages[primaryLanguage]) {
|
| 72 |
+
primaryLanguage = language;
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return primaryLanguage;
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* @typedef {import('./types').GistData} GistData Gist data.
|
| 81 |
+
*/
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Fetch GitHub gist information by given username and ID.
|
| 85 |
+
*
|
| 86 |
+
* @param {string} id GitHub gist ID.
|
| 87 |
+
* @returns {Promise<GistData>} Gist data.
|
| 88 |
+
*/
|
| 89 |
+
const fetchGist = async (id) => {
|
| 90 |
+
if (!id) {
|
| 91 |
+
throw new MissingParamError(["id"], "/api/gist?id=GIST_ID");
|
| 92 |
+
}
|
| 93 |
+
const res = await retryer(fetcher, { gistName: id });
|
| 94 |
+
if (res.data.errors) {
|
| 95 |
+
throw new Error(res.data.errors[0].message);
|
| 96 |
+
}
|
| 97 |
+
if (!res.data.data.viewer.gist) {
|
| 98 |
+
throw new Error("Gist not found");
|
| 99 |
+
}
|
| 100 |
+
const data = res.data.data.viewer.gist;
|
| 101 |
+
return {
|
| 102 |
+
name: data.files[Object.keys(data.files)[0]].name,
|
| 103 |
+
nameWithOwner: `${data.owner.login}/${
|
| 104 |
+
data.files[Object.keys(data.files)[0]].name
|
| 105 |
+
}`,
|
| 106 |
+
description: data.description,
|
| 107 |
+
language: calculatePrimaryLanguage(data.files),
|
| 108 |
+
starsCount: data.stargazerCount,
|
| 109 |
+
forksCount: data.forks.totalCount,
|
| 110 |
+
};
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
export { fetchGist };
|
| 114 |
+
export default fetchGist;
|
src/fetchers/repo.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { MissingParamError } from "../common/error.js";
|
| 4 |
+
import { request } from "../common/http.js";
|
| 5 |
+
import { retryer } from "../common/retryer.js";
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Repo data fetcher.
|
| 9 |
+
*
|
| 10 |
+
* @param {object} variables Fetcher variables.
|
| 11 |
+
* @param {string} token GitHub token.
|
| 12 |
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
| 13 |
+
*/
|
| 14 |
+
const fetcher = (variables, token) => {
|
| 15 |
+
return request(
|
| 16 |
+
{
|
| 17 |
+
query: `
|
| 18 |
+
fragment RepoInfo on Repository {
|
| 19 |
+
name
|
| 20 |
+
nameWithOwner
|
| 21 |
+
isPrivate
|
| 22 |
+
isArchived
|
| 23 |
+
isTemplate
|
| 24 |
+
stargazers {
|
| 25 |
+
totalCount
|
| 26 |
+
}
|
| 27 |
+
description
|
| 28 |
+
primaryLanguage {
|
| 29 |
+
color
|
| 30 |
+
id
|
| 31 |
+
name
|
| 32 |
+
}
|
| 33 |
+
forkCount
|
| 34 |
+
}
|
| 35 |
+
query getRepo($login: String!, $repo: String!) {
|
| 36 |
+
user(login: $login) {
|
| 37 |
+
repository(name: $repo) {
|
| 38 |
+
...RepoInfo
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
organization(login: $login) {
|
| 42 |
+
repository(name: $repo) {
|
| 43 |
+
...RepoInfo
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
`,
|
| 48 |
+
variables,
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
Authorization: `token ${token}`,
|
| 52 |
+
},
|
| 53 |
+
);
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* @typedef {import("./types").RepositoryData} RepositoryData Repository data.
|
| 60 |
+
*/
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Fetch repository data.
|
| 64 |
+
*
|
| 65 |
+
* @param {string} username GitHub username.
|
| 66 |
+
* @param {string} reponame GitHub repository name.
|
| 67 |
+
* @returns {Promise<RepositoryData>} Repository data.
|
| 68 |
+
*/
|
| 69 |
+
const fetchRepo = async (username, reponame) => {
|
| 70 |
+
if (!username && !reponame) {
|
| 71 |
+
throw new MissingParamError(["username", "repo"], urlExample);
|
| 72 |
+
}
|
| 73 |
+
if (!username) {
|
| 74 |
+
throw new MissingParamError(["username"], urlExample);
|
| 75 |
+
}
|
| 76 |
+
if (!reponame) {
|
| 77 |
+
throw new MissingParamError(["repo"], urlExample);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
let res = await retryer(fetcher, { login: username, repo: reponame });
|
| 81 |
+
|
| 82 |
+
const data = res.data.data;
|
| 83 |
+
|
| 84 |
+
if (!data.user && !data.organization) {
|
| 85 |
+
throw new Error("Not found");
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const isUser = data.organization === null && data.user;
|
| 89 |
+
const isOrg = data.user === null && data.organization;
|
| 90 |
+
|
| 91 |
+
if (isUser) {
|
| 92 |
+
if (!data.user.repository || data.user.repository.isPrivate) {
|
| 93 |
+
throw new Error("User Repository Not found");
|
| 94 |
+
}
|
| 95 |
+
return {
|
| 96 |
+
...data.user.repository,
|
| 97 |
+
starCount: data.user.repository.stargazers.totalCount,
|
| 98 |
+
};
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (isOrg) {
|
| 102 |
+
if (
|
| 103 |
+
!data.organization.repository ||
|
| 104 |
+
data.organization.repository.isPrivate
|
| 105 |
+
) {
|
| 106 |
+
throw new Error("Organization Repository Not found");
|
| 107 |
+
}
|
| 108 |
+
return {
|
| 109 |
+
...data.organization.repository,
|
| 110 |
+
starCount: data.organization.repository.stargazers.totalCount,
|
| 111 |
+
};
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
throw new Error("Unexpected behavior");
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
export { fetchRepo };
|
| 118 |
+
export default fetchRepo;
|
src/fetchers/stats.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import axios from "axios";
|
| 4 |
+
import * as dotenv from "dotenv";
|
| 5 |
+
import githubUsernameRegex from "github-username-regex";
|
| 6 |
+
import { calculateRank } from "../calculateRank.js";
|
| 7 |
+
import { retryer } from "../common/retryer.js";
|
| 8 |
+
import { logger } from "../common/log.js";
|
| 9 |
+
import { excludeRepositories } from "../common/envs.js";
|
| 10 |
+
import { CustomError, MissingParamError } from "../common/error.js";
|
| 11 |
+
import { wrapTextMultiline } from "../common/fmt.js";
|
| 12 |
+
import { request } from "../common/http.js";
|
| 13 |
+
|
| 14 |
+
dotenv.config();
|
| 15 |
+
|
| 16 |
+
// GraphQL queries.
|
| 17 |
+
const GRAPHQL_REPOS_FIELD = `
|
| 18 |
+
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
| 19 |
+
totalCount
|
| 20 |
+
nodes {
|
| 21 |
+
name
|
| 22 |
+
stargazers {
|
| 23 |
+
totalCount
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
pageInfo {
|
| 27 |
+
hasNextPage
|
| 28 |
+
endCursor
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
`;
|
| 32 |
+
|
| 33 |
+
const GRAPHQL_REPOS_QUERY = `
|
| 34 |
+
query userInfo($login: String!, $after: String) {
|
| 35 |
+
user(login: $login) {
|
| 36 |
+
${GRAPHQL_REPOS_FIELD}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
`;
|
| 40 |
+
|
| 41 |
+
const GRAPHQL_STATS_QUERY = `
|
| 42 |
+
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null) {
|
| 43 |
+
user(login: $login) {
|
| 44 |
+
name
|
| 45 |
+
login
|
| 46 |
+
commits: contributionsCollection (from: $startTime) {
|
| 47 |
+
totalCommitContributions,
|
| 48 |
+
}
|
| 49 |
+
reviews: contributionsCollection {
|
| 50 |
+
totalPullRequestReviewContributions
|
| 51 |
+
}
|
| 52 |
+
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
| 53 |
+
totalCount
|
| 54 |
+
}
|
| 55 |
+
pullRequests(first: 1) {
|
| 56 |
+
totalCount
|
| 57 |
+
}
|
| 58 |
+
mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
|
| 59 |
+
totalCount
|
| 60 |
+
}
|
| 61 |
+
openIssues: issues(states: OPEN) {
|
| 62 |
+
totalCount
|
| 63 |
+
}
|
| 64 |
+
closedIssues: issues(states: CLOSED) {
|
| 65 |
+
totalCount
|
| 66 |
+
}
|
| 67 |
+
followers {
|
| 68 |
+
totalCount
|
| 69 |
+
}
|
| 70 |
+
repositoryDiscussions @include(if: $includeDiscussions) {
|
| 71 |
+
totalCount
|
| 72 |
+
}
|
| 73 |
+
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
|
| 74 |
+
totalCount
|
| 75 |
+
}
|
| 76 |
+
${GRAPHQL_REPOS_FIELD}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
`;
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Stats fetcher object.
|
| 83 |
+
*
|
| 84 |
+
* @param {object & { after: string | null }} variables Fetcher variables.
|
| 85 |
+
* @param {string} token GitHub token.
|
| 86 |
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
| 87 |
+
*/
|
| 88 |
+
const fetcher = (variables, token) => {
|
| 89 |
+
const query = variables.after ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY;
|
| 90 |
+
return request(
|
| 91 |
+
{
|
| 92 |
+
query,
|
| 93 |
+
variables,
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
Authorization: `bearer ${token}`,
|
| 97 |
+
},
|
| 98 |
+
);
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Fetch stats information for a given username.
|
| 103 |
+
*
|
| 104 |
+
* @param {object} variables Fetcher variables.
|
| 105 |
+
* @param {string} variables.username GitHub username.
|
| 106 |
+
* @param {boolean} variables.includeMergedPullRequests Include merged pull requests.
|
| 107 |
+
* @param {boolean} variables.includeDiscussions Include discussions.
|
| 108 |
+
* @param {boolean} variables.includeDiscussionsAnswers Include discussions answers.
|
| 109 |
+
* @param {string|undefined} variables.startTime Time to start the count of total commits.
|
| 110 |
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
| 111 |
+
*
|
| 112 |
+
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
|
| 113 |
+
*/
|
| 114 |
+
const statsFetcher = async ({
|
| 115 |
+
username,
|
| 116 |
+
includeMergedPullRequests,
|
| 117 |
+
includeDiscussions,
|
| 118 |
+
includeDiscussionsAnswers,
|
| 119 |
+
startTime,
|
| 120 |
+
}) => {
|
| 121 |
+
let stats;
|
| 122 |
+
let hasNextPage = true;
|
| 123 |
+
let endCursor = null;
|
| 124 |
+
while (hasNextPage) {
|
| 125 |
+
const variables = {
|
| 126 |
+
login: username,
|
| 127 |
+
first: 100,
|
| 128 |
+
after: endCursor,
|
| 129 |
+
includeMergedPullRequests,
|
| 130 |
+
includeDiscussions,
|
| 131 |
+
includeDiscussionsAnswers,
|
| 132 |
+
startTime,
|
| 133 |
+
};
|
| 134 |
+
let res = await retryer(fetcher, variables);
|
| 135 |
+
if (res.data.errors) {
|
| 136 |
+
return res;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Store stats data.
|
| 140 |
+
const repoNodes = res.data.data.user.repositories.nodes;
|
| 141 |
+
if (stats) {
|
| 142 |
+
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
| 143 |
+
} else {
|
| 144 |
+
stats = res;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Disable multi page fetching on public Vercel instance due to rate limits.
|
| 148 |
+
const repoNodesWithStars = repoNodes.filter(
|
| 149 |
+
(node) => node.stargazers.totalCount !== 0,
|
| 150 |
+
);
|
| 151 |
+
hasNextPage =
|
| 152 |
+
process.env.FETCH_MULTI_PAGE_STARS === "true" &&
|
| 153 |
+
repoNodes.length === repoNodesWithStars.length &&
|
| 154 |
+
res.data.data.user.repositories.pageInfo.hasNextPage;
|
| 155 |
+
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
return stats;
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Fetch total commits using the REST API.
|
| 163 |
+
*
|
| 164 |
+
* @param {object} variables Fetcher variables.
|
| 165 |
+
* @param {string} token GitHub token.
|
| 166 |
+
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
|
| 167 |
+
*
|
| 168 |
+
* @see https://developer.github.com/v3/search/#search-commits
|
| 169 |
+
*/
|
| 170 |
+
const fetchTotalCommits = (variables, token) => {
|
| 171 |
+
return axios({
|
| 172 |
+
method: "get",
|
| 173 |
+
url: `https://api.github.com/search/commits?q=author:${variables.login}`,
|
| 174 |
+
headers: {
|
| 175 |
+
"Content-Type": "application/json",
|
| 176 |
+
Accept: "application/vnd.github.cloak-preview",
|
| 177 |
+
Authorization: `token ${token}`,
|
| 178 |
+
},
|
| 179 |
+
});
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
/**
|
| 183 |
+
* Fetch all the commits for all the repositories of a given username.
|
| 184 |
+
*
|
| 185 |
+
* @param {string} username GitHub username.
|
| 186 |
+
* @returns {Promise<number>} Total commits.
|
| 187 |
+
*
|
| 188 |
+
* @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
|
| 189 |
+
* #92#issuecomment-661026467 and #211 for more information.
|
| 190 |
+
*/
|
| 191 |
+
const totalCommitsFetcher = async (username) => {
|
| 192 |
+
if (!githubUsernameRegex.test(username)) {
|
| 193 |
+
logger.log("Invalid username provided.");
|
| 194 |
+
throw new Error("Invalid username provided.");
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
let res;
|
| 198 |
+
try {
|
| 199 |
+
res = await retryer(fetchTotalCommits, { login: username });
|
| 200 |
+
} catch (err) {
|
| 201 |
+
logger.log(err);
|
| 202 |
+
throw new Error(err);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
const totalCount = res.data.total_count;
|
| 206 |
+
if (!totalCount || isNaN(totalCount)) {
|
| 207 |
+
throw new CustomError(
|
| 208 |
+
"Could not fetch total commits.",
|
| 209 |
+
CustomError.GITHUB_REST_API_ERROR,
|
| 210 |
+
);
|
| 211 |
+
}
|
| 212 |
+
return totalCount;
|
| 213 |
+
};
|
| 214 |
+
|
| 215 |
+
/**
|
| 216 |
+
* Fetch stats for a given username.
|
| 217 |
+
*
|
| 218 |
+
* @param {string} username GitHub username.
|
| 219 |
+
* @param {boolean} include_all_commits Include all commits.
|
| 220 |
+
* @param {string[]} exclude_repo Repositories to exclude.
|
| 221 |
+
* @param {boolean} include_merged_pull_requests Include merged pull requests.
|
| 222 |
+
* @param {boolean} include_discussions Include discussions.
|
| 223 |
+
* @param {boolean} include_discussions_answers Include discussions answers.
|
| 224 |
+
* @param {number|undefined} commits_year Year to count total commits
|
| 225 |
+
* @returns {Promise<import("./types").StatsData>} Stats data.
|
| 226 |
+
*/
|
| 227 |
+
const fetchStats = async (
|
| 228 |
+
username,
|
| 229 |
+
include_all_commits = false,
|
| 230 |
+
exclude_repo = [],
|
| 231 |
+
include_merged_pull_requests = false,
|
| 232 |
+
include_discussions = false,
|
| 233 |
+
include_discussions_answers = false,
|
| 234 |
+
commits_year,
|
| 235 |
+
) => {
|
| 236 |
+
if (!username) {
|
| 237 |
+
throw new MissingParamError(["username"]);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
const stats = {
|
| 241 |
+
name: "",
|
| 242 |
+
totalPRs: 0,
|
| 243 |
+
totalPRsMerged: 0,
|
| 244 |
+
mergedPRsPercentage: 0,
|
| 245 |
+
totalReviews: 0,
|
| 246 |
+
totalCommits: 0,
|
| 247 |
+
totalIssues: 0,
|
| 248 |
+
totalStars: 0,
|
| 249 |
+
totalDiscussionsStarted: 0,
|
| 250 |
+
totalDiscussionsAnswered: 0,
|
| 251 |
+
contributedTo: 0,
|
| 252 |
+
rank: { level: "C", percentile: 100 },
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
let res = await statsFetcher({
|
| 256 |
+
username,
|
| 257 |
+
includeMergedPullRequests: include_merged_pull_requests,
|
| 258 |
+
includeDiscussions: include_discussions,
|
| 259 |
+
includeDiscussionsAnswers: include_discussions_answers,
|
| 260 |
+
startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined,
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
// Catch GraphQL errors.
|
| 264 |
+
if (res.data.errors) {
|
| 265 |
+
logger.error(res.data.errors);
|
| 266 |
+
if (res.data.errors[0].type === "NOT_FOUND") {
|
| 267 |
+
throw new CustomError(
|
| 268 |
+
res.data.errors[0].message || "Could not fetch user.",
|
| 269 |
+
CustomError.USER_NOT_FOUND,
|
| 270 |
+
);
|
| 271 |
+
}
|
| 272 |
+
if (res.data.errors[0].message) {
|
| 273 |
+
throw new CustomError(
|
| 274 |
+
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
|
| 275 |
+
res.statusText,
|
| 276 |
+
);
|
| 277 |
+
}
|
| 278 |
+
throw new CustomError(
|
| 279 |
+
"Something went wrong while trying to retrieve the stats data using the GraphQL API.",
|
| 280 |
+
CustomError.GRAPHQL_ERROR,
|
| 281 |
+
);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const user = res.data.data.user;
|
| 285 |
+
|
| 286 |
+
stats.name = user.name || user.login;
|
| 287 |
+
|
| 288 |
+
// if include_all_commits, fetch all commits using the REST API.
|
| 289 |
+
if (include_all_commits) {
|
| 290 |
+
stats.totalCommits = await totalCommitsFetcher(username);
|
| 291 |
+
} else {
|
| 292 |
+
stats.totalCommits = user.commits.totalCommitContributions;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
stats.totalPRs = user.pullRequests.totalCount;
|
| 296 |
+
if (include_merged_pull_requests) {
|
| 297 |
+
stats.totalPRsMerged = user.mergedPullRequests.totalCount;
|
| 298 |
+
stats.mergedPRsPercentage =
|
| 299 |
+
(user.mergedPullRequests.totalCount / user.pullRequests.totalCount) *
|
| 300 |
+
100 || 0;
|
| 301 |
+
}
|
| 302 |
+
stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
|
| 303 |
+
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
|
| 304 |
+
if (include_discussions) {
|
| 305 |
+
stats.totalDiscussionsStarted = user.repositoryDiscussions.totalCount;
|
| 306 |
+
}
|
| 307 |
+
if (include_discussions_answers) {
|
| 308 |
+
stats.totalDiscussionsAnswered =
|
| 309 |
+
user.repositoryDiscussionComments.totalCount;
|
| 310 |
+
}
|
| 311 |
+
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
| 312 |
+
|
| 313 |
+
// Retrieve stars while filtering out repositories to be hidden.
|
| 314 |
+
const allExcludedRepos = [...exclude_repo, ...excludeRepositories];
|
| 315 |
+
let repoToHide = new Set(allExcludedRepos);
|
| 316 |
+
|
| 317 |
+
stats.totalStars = user.repositories.nodes
|
| 318 |
+
.filter((data) => {
|
| 319 |
+
return !repoToHide.has(data.name);
|
| 320 |
+
})
|
| 321 |
+
.reduce((prev, curr) => {
|
| 322 |
+
return prev + curr.stargazers.totalCount;
|
| 323 |
+
}, 0);
|
| 324 |
+
|
| 325 |
+
stats.rank = calculateRank({
|
| 326 |
+
all_commits: include_all_commits,
|
| 327 |
+
commits: stats.totalCommits,
|
| 328 |
+
prs: stats.totalPRs,
|
| 329 |
+
reviews: stats.totalReviews,
|
| 330 |
+
issues: stats.totalIssues,
|
| 331 |
+
repos: user.repositories.totalCount,
|
| 332 |
+
stars: stats.totalStars,
|
| 333 |
+
followers: user.followers.totalCount,
|
| 334 |
+
});
|
| 335 |
+
|
| 336 |
+
return stats;
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
export { fetchStats };
|
| 340 |
+
export default fetchStats;
|
src/fetchers/top-languages.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { retryer } from "../common/retryer.js";
|
| 4 |
+
import { logger } from "../common/log.js";
|
| 5 |
+
import { excludeRepositories } from "../common/envs.js";
|
| 6 |
+
import { CustomError, MissingParamError } from "../common/error.js";
|
| 7 |
+
import { wrapTextMultiline } from "../common/fmt.js";
|
| 8 |
+
import { request } from "../common/http.js";
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Top languages fetcher object.
|
| 12 |
+
*
|
| 13 |
+
* @param {any} variables Fetcher variables.
|
| 14 |
+
* @param {string} token GitHub token.
|
| 15 |
+
* @returns {Promise<import("axios").AxiosResponse>} Languages fetcher response.
|
| 16 |
+
*/
|
| 17 |
+
const fetcher = (variables, token) => {
|
| 18 |
+
return request(
|
| 19 |
+
{
|
| 20 |
+
query: `
|
| 21 |
+
query userInfo($login: String!) {
|
| 22 |
+
user(login: $login) {
|
| 23 |
+
# fetch only owner repos & not forks
|
| 24 |
+
repositories(ownerAffiliations: OWNER, isFork: false, first: 100) {
|
| 25 |
+
nodes {
|
| 26 |
+
name
|
| 27 |
+
languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
|
| 28 |
+
edges {
|
| 29 |
+
size
|
| 30 |
+
node {
|
| 31 |
+
color
|
| 32 |
+
name
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
`,
|
| 41 |
+
variables,
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
Authorization: `token ${token}`,
|
| 45 |
+
},
|
| 46 |
+
);
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* @typedef {import("./types").TopLangData} TopLangData Top languages data.
|
| 51 |
+
*/
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Fetch top languages for a given username.
|
| 55 |
+
*
|
| 56 |
+
* @param {string} username GitHub username.
|
| 57 |
+
* @param {string[]} exclude_repo List of repositories to exclude.
|
| 58 |
+
* @param {number} size_weight Weightage to be given to size.
|
| 59 |
+
* @param {number} count_weight Weightage to be given to count.
|
| 60 |
+
* @returns {Promise<TopLangData>} Top languages data.
|
| 61 |
+
*/
|
| 62 |
+
const fetchTopLanguages = async (
|
| 63 |
+
username,
|
| 64 |
+
exclude_repo = [],
|
| 65 |
+
size_weight = 1,
|
| 66 |
+
count_weight = 0,
|
| 67 |
+
) => {
|
| 68 |
+
if (!username) {
|
| 69 |
+
throw new MissingParamError(["username"]);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const res = await retryer(fetcher, { login: username });
|
| 73 |
+
|
| 74 |
+
if (res.data.errors) {
|
| 75 |
+
logger.error(res.data.errors);
|
| 76 |
+
if (res.data.errors[0].type === "NOT_FOUND") {
|
| 77 |
+
throw new CustomError(
|
| 78 |
+
res.data.errors[0].message || "Could not fetch user.",
|
| 79 |
+
CustomError.USER_NOT_FOUND,
|
| 80 |
+
);
|
| 81 |
+
}
|
| 82 |
+
if (res.data.errors[0].message) {
|
| 83 |
+
throw new CustomError(
|
| 84 |
+
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
|
| 85 |
+
res.statusText,
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
throw new CustomError(
|
| 89 |
+
"Something went wrong while trying to retrieve the language data using the GraphQL API.",
|
| 90 |
+
CustomError.GRAPHQL_ERROR,
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
let repoNodes = res.data.data.user.repositories.nodes;
|
| 95 |
+
/** @type {Record<string, boolean>} */
|
| 96 |
+
let repoToHide = {};
|
| 97 |
+
const allExcludedRepos = [...exclude_repo, ...excludeRepositories];
|
| 98 |
+
|
| 99 |
+
// populate repoToHide map for quick lookup
|
| 100 |
+
// while filtering out
|
| 101 |
+
if (allExcludedRepos) {
|
| 102 |
+
allExcludedRepos.forEach((repoName) => {
|
| 103 |
+
repoToHide[repoName] = true;
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// filter out repositories to be hidden
|
| 108 |
+
repoNodes = repoNodes
|
| 109 |
+
.sort((a, b) => b.size - a.size)
|
| 110 |
+
.filter((name) => !repoToHide[name.name]);
|
| 111 |
+
|
| 112 |
+
let repoCount = 0;
|
| 113 |
+
|
| 114 |
+
repoNodes = repoNodes
|
| 115 |
+
.filter((node) => node.languages.edges.length > 0)
|
| 116 |
+
// flatten the list of language nodes
|
| 117 |
+
.reduce((acc, curr) => curr.languages.edges.concat(acc), [])
|
| 118 |
+
.reduce((acc, prev) => {
|
| 119 |
+
// get the size of the language (bytes)
|
| 120 |
+
let langSize = prev.size;
|
| 121 |
+
|
| 122 |
+
// if we already have the language in the accumulator
|
| 123 |
+
// & the current language name is same as previous name
|
| 124 |
+
// add the size to the language size and increase repoCount.
|
| 125 |
+
if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
|
| 126 |
+
langSize = prev.size + acc[prev.node.name].size;
|
| 127 |
+
repoCount += 1;
|
| 128 |
+
} else {
|
| 129 |
+
// reset repoCount to 1
|
| 130 |
+
// language must exist in at least one repo to be detected
|
| 131 |
+
repoCount = 1;
|
| 132 |
+
}
|
| 133 |
+
return {
|
| 134 |
+
...acc,
|
| 135 |
+
[prev.node.name]: {
|
| 136 |
+
name: prev.node.name,
|
| 137 |
+
color: prev.node.color,
|
| 138 |
+
size: langSize,
|
| 139 |
+
count: repoCount,
|
| 140 |
+
},
|
| 141 |
+
};
|
| 142 |
+
}, {});
|
| 143 |
+
|
| 144 |
+
Object.keys(repoNodes).forEach((name) => {
|
| 145 |
+
// comparison index calculation
|
| 146 |
+
repoNodes[name].size =
|
| 147 |
+
Math.pow(repoNodes[name].size, size_weight) *
|
| 148 |
+
Math.pow(repoNodes[name].count, count_weight);
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
const topLangs = Object.keys(repoNodes)
|
| 152 |
+
.sort((a, b) => repoNodes[b].size - repoNodes[a].size)
|
| 153 |
+
.reduce((result, key) => {
|
| 154 |
+
result[key] = repoNodes[key];
|
| 155 |
+
return result;
|
| 156 |
+
}, {});
|
| 157 |
+
|
| 158 |
+
return topLangs;
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
export { fetchTopLanguages };
|
| 162 |
+
export default fetchTopLanguages;
|
src/fetchers/types.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type GistData = {
|
| 2 |
+
name: string;
|
| 3 |
+
nameWithOwner: string;
|
| 4 |
+
description: string | null;
|
| 5 |
+
language: string | null;
|
| 6 |
+
starsCount: number;
|
| 7 |
+
forksCount: number;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
export type RepositoryData = {
|
| 11 |
+
name: string;
|
| 12 |
+
nameWithOwner: string;
|
| 13 |
+
isPrivate: boolean;
|
| 14 |
+
isArchived: boolean;
|
| 15 |
+
isTemplate: boolean;
|
| 16 |
+
stargazers: { totalCount: number };
|
| 17 |
+
description: string;
|
| 18 |
+
primaryLanguage: {
|
| 19 |
+
color: string;
|
| 20 |
+
id: string;
|
| 21 |
+
name: string;
|
| 22 |
+
};
|
| 23 |
+
forkCount: number;
|
| 24 |
+
starCount: number;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export type StatsData = {
|
| 28 |
+
name: string;
|
| 29 |
+
totalPRs: number;
|
| 30 |
+
totalPRsMerged: number;
|
| 31 |
+
mergedPRsPercentage: number;
|
| 32 |
+
totalReviews: number;
|
| 33 |
+
totalCommits: number;
|
| 34 |
+
totalIssues: number;
|
| 35 |
+
totalStars: number;
|
| 36 |
+
totalDiscussionsStarted: number;
|
| 37 |
+
totalDiscussionsAnswered: number;
|
| 38 |
+
contributedTo: number;
|
| 39 |
+
rank: { level: string; percentile: number };
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
export type Lang = {
|
| 43 |
+
name: string;
|
| 44 |
+
color: string;
|
| 45 |
+
size: number;
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
export type TopLangData = Record<string, Lang>;
|
| 49 |
+
|
| 50 |
+
export type WakaTimeData = {
|
| 51 |
+
categories: {
|
| 52 |
+
digital: string;
|
| 53 |
+
hours: number;
|
| 54 |
+
minutes: number;
|
| 55 |
+
name: string;
|
| 56 |
+
percent: number;
|
| 57 |
+
text: string;
|
| 58 |
+
total_seconds: number;
|
| 59 |
+
}[];
|
| 60 |
+
daily_average: number;
|
| 61 |
+
daily_average_including_other_language: number;
|
| 62 |
+
days_including_holidays: number;
|
| 63 |
+
days_minus_holidays: number;
|
| 64 |
+
editors: {
|
| 65 |
+
digital: string;
|
| 66 |
+
hours: number;
|
| 67 |
+
minutes: number;
|
| 68 |
+
name: string;
|
| 69 |
+
percent: number;
|
| 70 |
+
text: string;
|
| 71 |
+
total_seconds: number;
|
| 72 |
+
}[];
|
| 73 |
+
holidays: number;
|
| 74 |
+
human_readable_daily_average: string;
|
| 75 |
+
human_readable_daily_average_including_other_language: string;
|
| 76 |
+
human_readable_total: string;
|
| 77 |
+
human_readable_total_including_other_language: string;
|
| 78 |
+
id: string;
|
| 79 |
+
is_already_updating: boolean;
|
| 80 |
+
is_coding_activity_visible: boolean;
|
| 81 |
+
is_including_today: boolean;
|
| 82 |
+
is_other_usage_visible: boolean;
|
| 83 |
+
is_stuck: boolean;
|
| 84 |
+
is_up_to_date: boolean;
|
| 85 |
+
languages: {
|
| 86 |
+
digital: string;
|
| 87 |
+
hours: number;
|
| 88 |
+
minutes: number;
|
| 89 |
+
name: string;
|
| 90 |
+
percent: number;
|
| 91 |
+
text: string;
|
| 92 |
+
total_seconds: number;
|
| 93 |
+
}[];
|
| 94 |
+
operating_systems: {
|
| 95 |
+
digital: string;
|
| 96 |
+
hours: number;
|
| 97 |
+
minutes: number;
|
| 98 |
+
name: string;
|
| 99 |
+
percent: number;
|
| 100 |
+
text: string;
|
| 101 |
+
total_seconds: number;
|
| 102 |
+
}[];
|
| 103 |
+
percent_calculated: number;
|
| 104 |
+
range: string;
|
| 105 |
+
status: string;
|
| 106 |
+
timeout: number;
|
| 107 |
+
total_seconds: number;
|
| 108 |
+
total_seconds_including_other_language: number;
|
| 109 |
+
user_id: string;
|
| 110 |
+
username: string;
|
| 111 |
+
writes_only: boolean;
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
export type WakaTimeLang = {
|
| 115 |
+
name: string;
|
| 116 |
+
text: string;
|
| 117 |
+
percent: number;
|
| 118 |
+
};
|
src/fetchers/wakatime.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import axios from "axios";
|
| 4 |
+
import { CustomError, MissingParamError } from "../common/error.js";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* WakaTime data fetcher.
|
| 8 |
+
*
|
| 9 |
+
* @param {{username: string, api_domain: string }} props Fetcher props.
|
| 10 |
+
* @returns {Promise<import("./types").WakaTimeData>} WakaTime data response.
|
| 11 |
+
*/
|
| 12 |
+
const fetchWakatimeStats = async ({ username, api_domain }) => {
|
| 13 |
+
if (!username) {
|
| 14 |
+
throw new MissingParamError(["username"]);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const { data } = await axios.get(
|
| 19 |
+
`https://${
|
| 20 |
+
api_domain ? api_domain.replace(/\/$/gi, "") : "wakatime.com"
|
| 21 |
+
}/api/v1/users/${username}/stats?is_including_today=true`,
|
| 22 |
+
);
|
| 23 |
+
|
| 24 |
+
return data.data;
|
| 25 |
+
} catch (err) {
|
| 26 |
+
if (err.response.status < 200 || err.response.status > 299) {
|
| 27 |
+
throw new CustomError(
|
| 28 |
+
`Could not resolve to a User with the login of '${username}'`,
|
| 29 |
+
"WAKATIME_USER_NOT_FOUND",
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
throw err;
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
export { fetchWakatimeStats };
|
| 37 |
+
export default fetchWakatimeStats;
|
src/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from "./common/index.js";
|
| 2 |
+
export * from "./cards/index.js";
|
src/translations.js
ADDED
|
@@ -0,0 +1,1105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// @ts-check
|
| 2 |
+
|
| 3 |
+
import { encodeHTML } from "./common/html.js";
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Retrieves stat card labels in the available locales.
|
| 7 |
+
*
|
| 8 |
+
* @param {object} props Function arguments.
|
| 9 |
+
* @param {string} props.name The name of the locale.
|
| 10 |
+
* @param {string} props.apostrophe Whether to use apostrophe or not.
|
| 11 |
+
* @returns {object} The locales object.
|
| 12 |
+
*
|
| 13 |
+
* @see https://www.andiamo.co.uk/resources/iso-language-codes/ for language codes.
|
| 14 |
+
*/
|
| 15 |
+
const statCardLocales = ({ name, apostrophe }) => {
|
| 16 |
+
const encodedName = encodeHTML(name);
|
| 17 |
+
return {
|
| 18 |
+
"statcard.title": {
|
| 19 |
+
en: `${encodedName}'${apostrophe} GitHub Stats`,
|
| 20 |
+
ar: `${encodedName} إحصائيات جيت هاب`,
|
| 21 |
+
az: `${encodedName}'${apostrophe} Hesabının GitHub Statistikası`,
|
| 22 |
+
ca: `Estadístiques de GitHub de ${encodedName}`,
|
| 23 |
+
cn: `${encodedName} 的 GitHub 统计数据`,
|
| 24 |
+
"zh-tw": `${encodedName} 的 GitHub 統計資料`,
|
| 25 |
+
cs: `GitHub statistiky uživatele ${encodedName}`,
|
| 26 |
+
de: `${encodedName + apostrophe} GitHub-Statistiken`,
|
| 27 |
+
sw: `GitHub Stats za ${encodedName}`,
|
| 28 |
+
ur: `${encodedName} کے گٹ ہب کے اعداد و شمار`,
|
| 29 |
+
bg: `GitHub статистика на потребител ${encodedName}`,
|
| 30 |
+
bn: `${encodedName} এর GitHub পরিসংখ্যান`,
|
| 31 |
+
es: `Estadísticas de GitHub de ${encodedName}`,
|
| 32 |
+
fa: `آمار گیتهاب ${encodedName}`,
|
| 33 |
+
fi: `${encodedName}:n GitHub-tilastot`,
|
| 34 |
+
fr: `Statistiques GitHub de ${encodedName}`,
|
| 35 |
+
hi: `${encodedName} के GitHub आँकड़े`,
|
| 36 |
+
sa: `${encodedName} इत्यस्य GitHub सांख्यिकी`,
|
| 37 |
+
hu: `${encodedName} GitHub statisztika`,
|
| 38 |
+
it: `Statistiche GitHub di ${encodedName}`,
|
| 39 |
+
ja: `${encodedName}の GitHub 統計`,
|
| 40 |
+
kr: `${encodedName}의 GitHub 통계`,
|
| 41 |
+
nl: `${encodedName}'${apostrophe} GitHub-statistieken`,
|
| 42 |
+
"pt-pt": `Estatísticas do GitHub de ${encodedName}`,
|
| 43 |
+
"pt-br": `Estatísticas do GitHub de ${encodedName}`,
|
| 44 |
+
np: `${encodedName}'${apostrophe} गिटहब तथ्याङ्क`,
|
| 45 |
+
el: `Στατιστικά GitHub του ${encodedName}`,
|
| 46 |
+
ro: `Statisticile GitHub ale lui ${encodedName}`,
|
| 47 |
+
ru: `Статистика GitHub пользователя ${encodedName}`,
|
| 48 |
+
"uk-ua": `Статистика GitHub користувача ${encodedName}`,
|
| 49 |
+
id: `Statistik GitHub ${encodedName}`,
|
| 50 |
+
ml: `${encodedName}'${apostrophe} ഗിറ്റ്ഹബ് സ്ഥിതിവിവരക്കണക്കുകൾ`,
|
| 51 |
+
my: `${encodedName} ရဲ့ GitHub အခြေအနေများ`,
|
| 52 |
+
ta: `${encodedName} கிட்ஹப் புள்ளிவிவரங்கள்`,
|
| 53 |
+
sk: `GitHub štatistiky používateľa ${encodedName}`,
|
| 54 |
+
tr: `${encodedName} Hesabının GitHub İstatistikleri`,
|
| 55 |
+
pl: `Statystyki GitHub użytkownika ${encodedName}`,
|
| 56 |
+
uz: `${encodedName}ning GitHub'dagi statistikasi`,
|
| 57 |
+
vi: `Thống Kê GitHub ${encodedName}`,
|
| 58 |
+
se: `GitHubstatistik för ${encodedName}`,
|
| 59 |
+
he: `סטטיסטיקות הגיטהאב של ${encodedName}`,
|
| 60 |
+
fil: `Mga Stats ng GitHub ni ${encodedName}`,
|
| 61 |
+
th: `สถิติ GitHub ของ ${encodedName}`,
|
| 62 |
+
sr: `GitHub статистика корисника ${encodedName}`,
|
| 63 |
+
"sr-latn": `GitHub statistika korisnika ${encodedName}`,
|
| 64 |
+
no: `GitHub-statistikk for ${encodedName}`,
|
| 65 |
+
},
|
| 66 |
+
"statcard.ranktitle": {
|
| 67 |
+
en: `${encodedName}'${apostrophe} GitHub Rank`,
|
| 68 |
+
ar: `${encodedName} إحصائيات جيت هاب`,
|
| 69 |
+
az: `${encodedName}'${apostrophe} Hesabının GitHub Statistikası`,
|
| 70 |
+
ca: `Estadístiques de GitHub de ${encodedName}`,
|
| 71 |
+
cn: `${encodedName} 的 GitHub 统计数据`,
|
| 72 |
+
"zh-tw": `${encodedName} 的 GitHub 統計資料`,
|
| 73 |
+
cs: `GitHub statistiky uživatele ${encodedName}`,
|
| 74 |
+
de: `${encodedName + apostrophe} GitHub-Statistiken`,
|
| 75 |
+
sw: `GitHub Rank ya ${encodedName}`,
|
| 76 |
+
ur: `${encodedName} کی گٹ ہب رینک`,
|
| 77 |
+
bg: `GitHub ранг на ${encodedName}`,
|
| 78 |
+
bn: `${encodedName} এর GitHub পরিসংখ্যান`,
|
| 79 |
+
es: `Estadísticas de GitHub de ${encodedName}`,
|
| 80 |
+
fa: `رتبه گیتهاب ${encodedName}`,
|
| 81 |
+
fi: `${encodedName}:n GitHub-sijoitus`,
|
| 82 |
+
fr: `Statistiques GitHub de ${encodedName}`,
|
| 83 |
+
hi: `${encodedName} का GitHub स्थान`,
|
| 84 |
+
sa: `${encodedName} इत्यस्य GitHub स्थानम्`,
|
| 85 |
+
hu: `${encodedName} GitHub statisztika`,
|
| 86 |
+
it: `Statistiche GitHub di ${encodedName}`,
|
| 87 |
+
ja: `${encodedName} の GitHub ランク`,
|
| 88 |
+
kr: `${encodedName}의 GitHub 통계`,
|
| 89 |
+
nl: `${encodedName}'${apostrophe} GitHub-statistieken`,
|
| 90 |
+
"pt-pt": `Estatísticas do GitHub de ${encodedName}`,
|
| 91 |
+
"pt-br": `Estatísticas do GitHub de ${encodedName}`,
|
| 92 |
+
np: `${encodedName}'${apostrophe} गिटहब तथ्याङ्क`,
|
| 93 |
+
el: `Στατιστικά GitHub του ${encodedName}`,
|
| 94 |
+
ro: `Rankul GitHub al lui ${encodedName}`,
|
| 95 |
+
ru: `Рейтинг GitHub пользователя ${encodedName}`,
|
| 96 |
+
"uk-ua": `Рейтинг GitHub користувача ${encodedName}`,
|
| 97 |
+
id: `Statistik GitHub ${encodedName}`,
|
| 98 |
+
ml: `${encodedName}'${apostrophe} ഗിറ്റ്ഹബ് സ്ഥിതിവിവരക്കണക്കുകൾ`,
|
| 99 |
+
my: `${encodedName} ရဲ့ GitHub အဆင့်`,
|
| 100 |
+
ta: `${encodedName} கிட்ஹப் தரவரிசை`,
|
| 101 |
+
sk: `GitHub štatistiky používateľa ${encodedName}`,
|
| 102 |
+
tr: `${encodedName} Hesabının GitHub Yıldızları`,
|
| 103 |
+
pl: `Statystyki GitHub użytkownika ${encodedName}`,
|
| 104 |
+
uz: `${encodedName}ning GitHub'dagi statistikasi`,
|
| 105 |
+
vi: `Thống Kê GitHub ${encodedName}`,
|
| 106 |
+
se: `GitHubstatistik för ${encodedName}`,
|
| 107 |
+
he: `דרגת הגיטהאב של ${encodedName}`,
|
| 108 |
+
fil: `Ranggo ng GitHub ni ${encodedName}`,
|
| 109 |
+
th: `อันดับ GitHub ของ ${encodedName}`,
|
| 110 |
+
sr: `Ранк корисника ${encodedName}`,
|
| 111 |
+
"sr-latn": `Rank korisnika ${encodedName}`,
|
| 112 |
+
no: `GitHub-statistikk for ${encodedName}`,
|
| 113 |
+
},
|
| 114 |
+
"statcard.totalstars": {
|
| 115 |
+
en: "Total Stars Earned",
|
| 116 |
+
ar: "مجموع النجوم",
|
| 117 |
+
az: "Ümumi Ulduz",
|
| 118 |
+
ca: "Total d'estrelles",
|
| 119 |
+
cn: "获标星数",
|
| 120 |
+
"zh-tw": "得標星星數量(Star)",
|
| 121 |
+
cs: "Celkem hvězd",
|
| 122 |
+
de: "Insgesamt erhaltene Sterne",
|
| 123 |
+
sw: "Medali(stars) ulizojishindia",
|
| 124 |
+
ur: "کل ستارے حاصل کیے",
|
| 125 |
+
bg: "Получени звезди",
|
| 126 |
+
bn: "সর্বমোট Star",
|
| 127 |
+
es: "Estrellas totales",
|
| 128 |
+
fa: "مجموع ستارههای دریافتشده",
|
| 129 |
+
fi: "Ansaitut tähdet yhteensä",
|
| 130 |
+
fr: "Total d'étoiles",
|
| 131 |
+
hi: "कुल अर्जित सितारे",
|
| 132 |
+
sa: "अर्जिताः कुल-तारकाः",
|
| 133 |
+
hu: "Csillagok",
|
| 134 |
+
it: "Stelle totali",
|
| 135 |
+
ja: "スターされた数",
|
| 136 |
+
kr: "받은 스타 수",
|
| 137 |
+
nl: "Totaal Sterren Ontvangen",
|
| 138 |
+
"pt-pt": "Total de estrelas",
|
| 139 |
+
"pt-br": "Total de estrelas",
|
| 140 |
+
np: "कुल ताराहरू",
|
| 141 |
+
el: "Σύνολο Αστεριών",
|
| 142 |
+
ro: "Total de stele câștigate",
|
| 143 |
+
ru: "Всего звёзд",
|
| 144 |
+
"uk-ua": "Всього зірок",
|
| 145 |
+
id: "Total Bintang",
|
| 146 |
+
ml: "ആകെ നക്ഷത്രങ്ങൾ",
|
| 147 |
+
my: "စုစုပေါင်းကြယ်များ",
|
| 148 |
+
ta: "சம்பாதித்த மொத்த நட்சத்திரங்கள்",
|
| 149 |
+
sk: "Hviezdy",
|
| 150 |
+
tr: "Toplam Yıldız",
|
| 151 |
+
pl: "Liczba otrzymanych gwiazdek",
|
| 152 |
+
uz: "Yulduzchalar",
|
| 153 |
+
vi: "Tổng Số Sao",
|
| 154 |
+
se: "Antal intjänade stjärnor",
|
| 155 |
+
he: "סך כל הכוכבים שהושגו",
|
| 156 |
+
fil: "Kabuuang Nakuhang Bituin",
|
| 157 |
+
th: "ดาวทั้งหมดที่ได้รับ",
|
| 158 |
+
sr: "Број освојених звездица",
|
| 159 |
+
"sr-latn": "Broj osvojenih zvezdica",
|
| 160 |
+
no: "Totalt antall stjerner",
|
| 161 |
+
},
|
| 162 |
+
"statcard.commits": {
|
| 163 |
+
en: "Total Commits",
|
| 164 |
+
ar: "مجموع المساهمات",
|
| 165 |
+
az: "Ümumi Commit",
|
| 166 |
+
ca: "Commits totals",
|
| 167 |
+
cn: "累计提交总数",
|
| 168 |
+
"zh-tw": "累計提交數量(Commit)",
|
| 169 |
+
cs: "Celkem commitů",
|
| 170 |
+
de: "Anzahl Commits",
|
| 171 |
+
sw: "Matendo yako yote",
|
| 172 |
+
ur: "کل کمٹ",
|
| 173 |
+
bg: "Общо ангажименти",
|
| 174 |
+
bn: "সর্বমোট Commit",
|
| 175 |
+
es: "Commits totales",
|
| 176 |
+
fa: "مجموع کامیتها",
|
| 177 |
+
fi: "Yhteensä committeja",
|
| 178 |
+
fr: "Total des Commits",
|
| 179 |
+
hi: "कुल commits",
|
| 180 |
+
sa: "कुल-समिन्चयः",
|
| 181 |
+
hu: "Összes commit",
|
| 182 |
+
it: "Commit totali",
|
| 183 |
+
ja: "合計コミット数",
|
| 184 |
+
kr: "전체 커밋 수",
|
| 185 |
+
nl: "Aantal commits",
|
| 186 |
+
"pt-pt": "Total de Commits",
|
| 187 |
+
"pt-br": "Total de Commits",
|
| 188 |
+
np: "कुल Commits",
|
| 189 |
+
el: "Σύνολο Commits",
|
| 190 |
+
ro: "Total Commit-uri",
|
| 191 |
+
ru: "Всего коммитов",
|
| 192 |
+
"uk-ua": "Всього комітів",
|
| 193 |
+
id: "Total Komitmen",
|
| 194 |
+
ml: "ആകെ കമ്മിറ്റുകൾ",
|
| 195 |
+
my: "စုစုပေါင်း Commit များ",
|
| 196 |
+
ta: `மொத்த கமிட்கள்`,
|
| 197 |
+
sk: "Všetky commity",
|
| 198 |
+
tr: "Toplam Commit",
|
| 199 |
+
pl: "Wszystkie commity",
|
| 200 |
+
uz: "'Commit'lar",
|
| 201 |
+
vi: "Tổng Số Cam Kết",
|
| 202 |
+
se: "Totalt antal commits",
|
| 203 |
+
he: "סך כל ה־commits",
|
| 204 |
+
fil: "Kabuuang Commits",
|
| 205 |
+
th: "Commit ทั้งหมด",
|
| 206 |
+
sr: "Укупно commit-ова",
|
| 207 |
+
"sr-latn": "Ukupno commit-ova",
|
| 208 |
+
no: "Totalt antall commits",
|
| 209 |
+
},
|
| 210 |
+
"statcard.prs": {
|
| 211 |
+
en: "Total PRs",
|
| 212 |
+
ar: "مجموع طلبات السحب",
|
| 213 |
+
az: "Ümumi PR",
|
| 214 |
+
ca: "PRs totals",
|
| 215 |
+
cn: "发起的 PR 总数",
|
| 216 |
+
"zh-tw": "拉取請求數量���PR)",
|
| 217 |
+
cs: "Celkem PRs",
|
| 218 |
+
de: "PRs Insgesamt",
|
| 219 |
+
sw: "PRs Zote",
|
| 220 |
+
ur: "کل پی آرز",
|
| 221 |
+
bg: "Заявки за изтегляния",
|
| 222 |
+
bn: "সর্বমোট PR",
|
| 223 |
+
es: "PRs totales",
|
| 224 |
+
fa: "مجموع Pull Request",
|
| 225 |
+
fi: "Yhteensä PR:t",
|
| 226 |
+
fr: "Total des PRs",
|
| 227 |
+
hi: "कुल PR",
|
| 228 |
+
sa: "कुल-पीआर",
|
| 229 |
+
hu: "Összes PR",
|
| 230 |
+
it: "PR totali",
|
| 231 |
+
ja: "合計 PR",
|
| 232 |
+
kr: "PR 횟수",
|
| 233 |
+
nl: "Aantal PR's",
|
| 234 |
+
"pt-pt": "Total de PRs",
|
| 235 |
+
"pt-br": "Total de PRs",
|
| 236 |
+
np: "कुल PRs",
|
| 237 |
+
el: "Σύνολο PRs",
|
| 238 |
+
ro: "Total PR-uri",
|
| 239 |
+
ru: "Всего запросов изменений",
|
| 240 |
+
"uk-ua": "Всього запитів на злиття",
|
| 241 |
+
id: "Total Permintaan Tarik",
|
| 242 |
+
ml: "ആകെ പുൾ അഭ്യർത്ഥനകൾ",
|
| 243 |
+
my: "စုစုပေါင်း PR များ",
|
| 244 |
+
ta: `மொத்த இழுக்கும் கோரிக்கைகள்`,
|
| 245 |
+
sk: "Všetky PR",
|
| 246 |
+
tr: "Toplam PR",
|
| 247 |
+
pl: "Wszystkie PR-y",
|
| 248 |
+
uz: "'Pull Request'lar",
|
| 249 |
+
vi: "Tổng Số PR",
|
| 250 |
+
se: "Totalt antal PR",
|
| 251 |
+
he: "סך כל ה־PRs",
|
| 252 |
+
fil: "Kabuuang PRs",
|
| 253 |
+
th: "PR ทั้งหมด",
|
| 254 |
+
sr: "Укупно PR-ова",
|
| 255 |
+
"sr-latn": "Ukupno PR-ova",
|
| 256 |
+
no: "Totalt antall PR",
|
| 257 |
+
},
|
| 258 |
+
"statcard.issues": {
|
| 259 |
+
en: "Total Issues",
|
| 260 |
+
ar: "مجموع التحسينات",
|
| 261 |
+
az: "Ümumi Problem",
|
| 262 |
+
ca: "Issues totals",
|
| 263 |
+
cn: "提出的 issue 总数",
|
| 264 |
+
"zh-tw": "提出問題數量(Issue)",
|
| 265 |
+
cs: "Celkem problémů",
|
| 266 |
+
de: "Anzahl Issues",
|
| 267 |
+
sw: "Masuala Ibuka",
|
| 268 |
+
ur: "کل مسائل",
|
| 269 |
+
bg: "Брой въпроси",
|
| 270 |
+
bn: "সর্বমোট Issue",
|
| 271 |
+
es: "Issues totales",
|
| 272 |
+
fa: "مجموع مسائل",
|
| 273 |
+
fi: "Yhteensä ongelmat",
|
| 274 |
+
fr: "Nombre total d'incidents",
|
| 275 |
+
hi: "कुल मुद्दे(Issues)",
|
| 276 |
+
sa: "कुल-समस्याः",
|
| 277 |
+
hu: "Összes hibajegy",
|
| 278 |
+
it: "Segnalazioni totali",
|
| 279 |
+
ja: "合計 issue",
|
| 280 |
+
kr: "이슈 개수",
|
| 281 |
+
nl: "Aantal kwesties",
|
| 282 |
+
"pt-pt": "Total de Issues",
|
| 283 |
+
"pt-br": "Total de Issues",
|
| 284 |
+
np: "कुल मुद्दाहरू",
|
| 285 |
+
el: "Σύνολο Ζητημάτων",
|
| 286 |
+
ro: "Total Issue-uri",
|
| 287 |
+
ru: "Всего вопросов",
|
| 288 |
+
"uk-ua": "Всього питань",
|
| 289 |
+
id: "Total Masalah Dilaporkan",
|
| 290 |
+
ml: "ആകെ പ്രശ്നങ്ങൾ",
|
| 291 |
+
my: "စုစုပေါင်းပြဿနာများ",
|
| 292 |
+
ta: `மொத்த சிக்கல்கள்`,
|
| 293 |
+
sk: "Všetky problémy",
|
| 294 |
+
tr: "Toplam Hata",
|
| 295 |
+
pl: "Wszystkie problemy",
|
| 296 |
+
uz: "'Issue'lar",
|
| 297 |
+
vi: "Tổng Số Vấn Đề",
|
| 298 |
+
se: "Total antal issues",
|
| 299 |
+
he: "סך כל ה־issues",
|
| 300 |
+
fil: "Kabuuang mga Isyu",
|
| 301 |
+
th: "Issue ทั้งหมด",
|
| 302 |
+
sr: "Укупно пријављених проблема",
|
| 303 |
+
"sr-latn": "Ukupno prijavljenih problema",
|
| 304 |
+
no: "Totalt antall issues",
|
| 305 |
+
},
|
| 306 |
+
"statcard.contribs": {
|
| 307 |
+
en: "Contributed to (last year)",
|
| 308 |
+
ar: "ساهم في (العام الماضي)",
|
| 309 |
+
az: "Töhfə verdi (ötən il)",
|
| 310 |
+
ca: "Contribucions (l'any passat)",
|
| 311 |
+
cn: "贡献的项目数(去年)",
|
| 312 |
+
"zh-tw": "參與項目數量(去年)",
|
| 313 |
+
cs: "Přispěl k (minulý rok)",
|
| 314 |
+
de: "Beigetragen zu (letztes Jahr)",
|
| 315 |
+
sw: "Idadi ya michango (mwaka mzima)",
|
| 316 |
+
ur: "پچھلے سال میں تعاون کیا",
|
| 317 |
+
bg: "Приноси (за изминалата година)",
|
| 318 |
+
bn: "অবদান (গত বছর)",
|
| 319 |
+
es: "Contribuciones en (el año pasado)",
|
| 320 |
+
fa: "مشارکت در (سال گذشته)",
|
| 321 |
+
fi: "Osallistunut (viime vuonna)",
|
| 322 |
+
fr: "Contribué à (l'année dernière)",
|
| 323 |
+
hi: "(पिछले वर्ष) में योगदान दिया",
|
| 324 |
+
sa: "(गते वर्षे) योगदानम् कृतम्",
|
| 325 |
+
hu: "Hozzájárulások (tavaly)",
|
| 326 |
+
it: "Ha contribuito a (l'anno scorso)",
|
| 327 |
+
ja: "貢献したリポジトリ (昨年)",
|
| 328 |
+
kr: "(작년) 기여",
|
| 329 |
+
nl: "Bijgedragen aan (vorig jaar)",
|
| 330 |
+
"pt-pt": "Contribuiu em (ano passado)",
|
| 331 |
+
"pt-br": "Contribuiu para (ano passado)",
|
| 332 |
+
np: "कुल योगदानहरू (गत वर्ष)",
|
| 333 |
+
el: "Συνεισφέρθηκε σε (πέρυσι)",
|
| 334 |
+
ro: "Total Contribuiri",
|
| 335 |
+
ru: "Внесено вклада (за прошлый год)",
|
| 336 |
+
"uk-ua": "Зроблено внесок (за минулий рік)",
|
| 337 |
+
id: "Berkontribusi ke (tahun lalu)",
|
| 338 |
+
ml: "(കഴിഞ്ഞ വർഷത്തെ)ആകെ സംഭാവനകൾ ",
|
| 339 |
+
my: "အကူအညီပေးခဲ့သည် (ပြီးခဲ့သည့်နှစ်)",
|
| 340 |
+
ta: "(கடந்த ஆண்டு) பங்களித்தது",
|
| 341 |
+
sk: "Účasti (minulý rok)",
|
| 342 |
+
tr: "Katkı Verildi (geçen yıl)",
|
| 343 |
+
pl: "Kontrybucje (w zeszłym roku)",
|
| 344 |
+
uz: "Hissa qoʻshgan (o'tgan yili)",
|
| 345 |
+
vi: "Đã Đóng Góp (năm ngoái)",
|
| 346 |
+
se: "Bidragit till (förra året)",
|
| 347 |
+
he: "תרם ל... (שנה שעברה)",
|
| 348 |
+
fil: "Nag-ambag sa (nakaraang taon)",
|
| 349 |
+
th: "มีส่วนร่วมใน (ปีที่แล้ว)",
|
| 350 |
+
sr: "Доприноси (прошла година)",
|
| 351 |
+
"sr-latn": "Doprinosi (prošla godina)",
|
| 352 |
+
no: "Bidro til (i fjor)",
|
| 353 |
+
},
|
| 354 |
+
"statcard.reviews": {
|
| 355 |
+
en: "Total PRs Reviewed",
|
| 356 |
+
ar: "طلبات السحب التي تم مراجعتها",
|
| 357 |
+
az: "Nəzərdən Keçirilən Ümumi PR",
|
| 358 |
+
ca: "Total de PRs revisats",
|
| 359 |
+
cn: "审查的 PR 总数",
|
| 360 |
+
"zh-tw": "審核的 PR 總計",
|
| 361 |
+
cs: "Celkový počet PR",
|
| 362 |
+
de: "Insgesamt überprüfte PRs",
|
| 363 |
+
sw: "Idadi ya PRs zilizopitiliwa upya",
|
| 364 |
+
ur: "کل پی آرز کا جائزہ لیا",
|
| 365 |
+
bg: "Разгледани заявки за изтегляне",
|
| 366 |
+
bn: "সর্বমোট পুনরালোচনা করা PR",
|
| 367 |
+
es: "PR totales revisados",
|
| 368 |
+
fa: "مجموع درخواستهای ادغام بررسیشده",
|
| 369 |
+
fi: "Yhteensä tarkastettuja PR:itä",
|
| 370 |
+
fr: "Nombre total de PR examinés",
|
| 371 |
+
hi: "कुल PRs की समीक्षा की गई",
|
| 372 |
+
sa: "समीक्षिताः कुल-पीआर",
|
| 373 |
+
hu: "Összes ellenőrzött PR",
|
| 374 |
+
it: "PR totali esaminati",
|
| 375 |
+
ja: "レビューされた PR の総数",
|
| 376 |
+
kr: "검토된 총 PR",
|
| 377 |
+
nl: "Totaal beoordeelde PR's",
|
| 378 |
+
"pt-pt": "Total de PRs revistos",
|
| 379 |
+
"pt-br": "Total de PRs revisados",
|
| 380 |
+
np: "कुल पीआर समीक्षित",
|
| 381 |
+
el: "Σύνολο Αναθεωρημένων PR",
|
| 382 |
+
ro: "Total PR-uri Revizuite",
|
| 383 |
+
ru: "Всего запросов проверено",
|
| 384 |
+
"uk-ua": "Всього запитів перевірено",
|
| 385 |
+
id: "Total PR yang Direview",
|
| 386 |
+
ml: "ആകെ പുൾ അവലോകനങ്ങൾ",
|
| 387 |
+
my: "စုစုပေါင်း PR များကို ပြန်လည်သုံးသပ်ခဲ့မှု",
|
| 388 |
+
ta: "மதிப்பாய்வு செய்யப்பட்ட மொத்த இழுத்தல் கோரிக்கைகள்",
|
| 389 |
+
sk: "Celkový počet PR",
|
| 390 |
+
tr: "İncelenen toplam PR",
|
| 391 |
+
pl: "Łącznie sprawdzonych PR",
|
| 392 |
+
uz: "Koʻrib chiqilgan PR-lar soni",
|
| 393 |
+
vi: "Tổng Số PR Đã Xem Xét",
|
| 394 |
+
se: "Totalt antal granskade PR",
|
| 395 |
+
he: "סך כל ה־PRs שנסרקו",
|
| 396 |
+
fil: "Kabuuang PR na Na-review",
|
| 397 |
+
th: "รีวิว PR แล้วทั้งหมด",
|
| 398 |
+
sr: "Укупно прегледаних PR-ова",
|
| 399 |
+
"sr-latn": "Ukupno pregledanih PR-ova",
|
| 400 |
+
no: "Totalt antall vurderte PR",
|
| 401 |
+
},
|
| 402 |
+
"statcard.discussions-started": {
|
| 403 |
+
en: "Total Discussions Started",
|
| 404 |
+
ar: "مجموع المناقشات التي بدأها",
|
| 405 |
+
az: "Başladılan Ümumi Müzakirə",
|
| 406 |
+
ca: "Discussions totals iniciades",
|
| 407 |
+
cn: "发起的讨论总数",
|
| 408 |
+
"zh-tw": "發起的討論總數",
|
| 409 |
+
cs: "Celkem zahájených diskusí",
|
| 410 |
+
de: "Gesamt gestartete Diskussionen",
|
| 411 |
+
sw: "Idadi ya majadiliano yaliyoanzishwa",
|
| 412 |
+
ur: "کل مباحثے شروع کیے",
|
| 413 |
+
bg: "Започнати дискусии",
|
| 414 |
+
bn: "সর্বমোট আলোচনা শুরু",
|
| 415 |
+
es: "Discusiones totales iniciadas",
|
| 416 |
+
fa: "مجموع بحثهای آغازشده",
|
| 417 |
+
fi: "Aloitetut keskustelut yhteensä",
|
| 418 |
+
fr: "Nombre total de discussions lancées",
|
| 419 |
+
hi: "कुल चर्चाएँ शुरू हुईं",
|
| 420 |
+
sa: "प्रारब्धाः कुल-चर्चाः",
|
| 421 |
+
hu: "Összes megkezdett megbeszélés",
|
| 422 |
+
it: "Discussioni totali avviate",
|
| 423 |
+
ja: "開始されたディスカッションの総数",
|
| 424 |
+
kr: "시작된 토론 총 수",
|
| 425 |
+
nl: "Totaal gestarte discussies",
|
| 426 |
+
"pt-pt": "Total de Discussões Iniciadas",
|
| 427 |
+
"pt-br": "Total de Discussões Iniciadas",
|
| 428 |
+
np: "कुल चर्चा सुरु",
|
| 429 |
+
el: "Σύνολο Συζητήσεων που Ξεκίνησαν",
|
| 430 |
+
ro: "Total Discuții Începute",
|
| 431 |
+
ru: "Всего начатых обсуждений",
|
| 432 |
+
"uk-ua": "Всього розпочатих дискусій",
|
| 433 |
+
id: "Total Diskusi Dimulai",
|
| 434 |
+
ml: "ആരംഭിച്ച ആലോചനകൾ",
|
| 435 |
+
my: "စုစုပေါင်း စတင်ခဲ့သော ဆွေးနွေးမှုများ",
|
| 436 |
+
ta: "மொத்த விவாதங்கள் தொடங்கின",
|
| 437 |
+
sk: "Celkový počet začatých diskusií",
|
| 438 |
+
tr: "Başlatılan Toplam Tartışma",
|
| 439 |
+
pl: "Łącznie rozpoczętych dyskusji",
|
| 440 |
+
uz: "Boshlangan muzokaralar soni",
|
| 441 |
+
vi: "Tổng Số Thảo Luận Bắt Đầu",
|
| 442 |
+
se: "Totalt antal diskussioner startade",
|
| 443 |
+
he: "סך כל הדיונים שהותחלו",
|
| 444 |
+
fil: "Kabuuang mga Diskusyon na Sinimulan",
|
| 445 |
+
th: "เริ่มหัวข้อสนทนาทั้งหมด",
|
| 446 |
+
sr: "Укупно покренутих дискусија",
|
| 447 |
+
"sr-latn": "Ukupno pokrenutih diskusija",
|
| 448 |
+
no: "Totalt antall startede diskusjoner",
|
| 449 |
+
},
|
| 450 |
+
"statcard.discussions-answered": {
|
| 451 |
+
en: "Total Discussions Answered",
|
| 452 |
+
ar: "مجموع المناقشات المُجابة",
|
| 453 |
+
az: "Cavablandırılan Ümumi Müzakirə",
|
| 454 |
+
ca: "Discussions totals respostes",
|
| 455 |
+
cn: "回复的讨论总数",
|
| 456 |
+
"zh-tw": "回覆討論總計",
|
| 457 |
+
cs: "Celkem zodpovězených diskusí",
|
| 458 |
+
de: "Gesamt beantwortete Diskussionen",
|
| 459 |
+
sw: "Idadi ya majadiliano yaliyojibiwa",
|
| 460 |
+
ur: "کل مباحثے جواب دیے",
|
| 461 |
+
bg: "Отговорени дискусии",
|
| 462 |
+
bn: "সর্বমোট আলোচনা উত্তর",
|
| 463 |
+
es: "Discusiones totales respondidas",
|
| 464 |
+
fa: "مجموع بحثهای پاسخدادهشده",
|
| 465 |
+
fi: "Vastatut keskustelut yhteensä",
|
| 466 |
+
fr: "Nombre total de discussions répondues",
|
| 467 |
+
hi: "कुल चर्चाओं के उत्तर",
|
| 468 |
+
sa: "उत्तरिताः कुल-चर्चाः",
|
| 469 |
+
hu: "Összes megválaszolt megbeszélés",
|
| 470 |
+
it: "Discussioni totali risposte",
|
| 471 |
+
ja: "回答されたディスカッションの総数",
|
| 472 |
+
kr: "답변된 토론 총 수",
|
| 473 |
+
nl: "Totaal beantwoorde discussies",
|
| 474 |
+
"pt-pt": "Total de Discussões Respondidas",
|
| 475 |
+
"pt-br": "Total de Discussões Respondidas",
|
| 476 |
+
np: "कुल चर्चा उत्तर",
|
| 477 |
+
el: "Σύνολο Συζητήσεων που Απαντήθηκαν",
|
| 478 |
+
ro: "Total Răspunsuri La Discuții",
|
| 479 |
+
ru: "Всего отвеченных обсуждений",
|
| 480 |
+
"uk-ua": "Всього відповідей на дискусії",
|
| 481 |
+
id: "Total Diskusi Dibalas",
|
| 482 |
+
ml: "ഉത്തരം നൽകിയ ആലോചനകൾ",
|
| 483 |
+
my: "စုစုပေါင်း ပြန်လည်ဖြေကြားခဲ့သော ဆွေးနွေးမှုများ",
|
| 484 |
+
ta: "பதிலளிக்கப்பட்ட மொத்த விவாதங்கள்",
|
| 485 |
+
sk: "Celkový počet zodpovedaných diskusií",
|
| 486 |
+
tr: "Toplam Cevaplanan Tartışma",
|
| 487 |
+
pl: "Łącznie odpowiedzianych dyskusji",
|
| 488 |
+
uz: "Javob berilgan muzokaralar soni",
|
| 489 |
+
vi: "Tổng Số Thảo Luận Đã Trả Lời",
|
| 490 |
+
se: "Totalt antal diskussioner besvarade",
|
| 491 |
+
he: "סך כל הדיונים שנענו",
|
| 492 |
+
fil: "Kabuuang mga Diskusyon na Sinagot",
|
| 493 |
+
th: "ตอบกลับหัวข้อสนทนาทั้งหมด",
|
| 494 |
+
sr: "Укупно одговорених дискусија",
|
| 495 |
+
"sr-latn": "Ukupno odgovorenih diskusija",
|
| 496 |
+
no: "Totalt antall besvarte diskusjoner",
|
| 497 |
+
},
|
| 498 |
+
"statcard.prs-merged": {
|
| 499 |
+
en: "Total PRs Merged",
|
| 500 |
+
ar: "مجموع طلبات السحب المُدمجة",
|
| 501 |
+
az: "Birləşdirilmiş Ümumi PR",
|
| 502 |
+
ca: "PRs totals fusionats",
|
| 503 |
+
cn: "合并的 PR 总数",
|
| 504 |
+
"zh-tw": "合併的 PR 總計",
|
| 505 |
+
cs: "Celkem sloučených PR",
|
| 506 |
+
de: "Insgesamt zusammengeführte PRs",
|
| 507 |
+
sw: "Idadi ya PRs zilizounganishwa",
|
| 508 |
+
ur: "کل پی آرز ضم کیے",
|
| 509 |
+
bg: "Сляти заявки за изтегляния",
|
| 510 |
+
bn: "সর্বমোট PR একত্রীকৃত",
|
| 511 |
+
es: "PR totales fusionados",
|
| 512 |
+
fa: "مجموع درخواستهای ادغام شده",
|
| 513 |
+
fi: "Yhteensä yhdistetyt PR:t",
|
| 514 |
+
fr: "Nombre total de PR fusionnés",
|
| 515 |
+
hi: "कुल PR का विलय",
|
| 516 |
+
sa: "विलीनाः कुल-पीआर",
|
| 517 |
+
hu: "Összes egyesített PR",
|
| 518 |
+
it: "PR totali uniti",
|
| 519 |
+
ja: "マージされた PR の総数",
|
| 520 |
+
kr: "병합된 총 PR",
|
| 521 |
+
nl: "Totaal samengevoegde PR's",
|
| 522 |
+
"pt-pt": "Total de PRs Fundidos",
|
| 523 |
+
"pt-br": "Total de PRs Integrados",
|
| 524 |
+
np: "कुल विलयित PRs",
|
| 525 |
+
el: "Σύνολο Συγχωνευμένων PR",
|
| 526 |
+
ro: "Total PR-uri Fuzionate",
|
| 527 |
+
ru: "Всего объединённых запросов",
|
| 528 |
+
"uk-ua": "Всього об'єднаних запитів",
|
| 529 |
+
id: "Total PR Digabungkan",
|
| 530 |
+
my: "စုစုပေါင်း ပေါင်းစည်းခဲ့သော PR များ",
|
| 531 |
+
ta: "இணைக்கப்பட்ட மொத்த PRகள்",
|
| 532 |
+
sk: "Celkový počet zlúčených PR",
|
| 533 |
+
tr: "Toplam Birleştirilmiş PR",
|
| 534 |
+
pl: "Łącznie połączonych PR",
|
| 535 |
+
uz: "Birlangan PR-lar soni",
|
| 536 |
+
vi: "Tổng Số PR Đã Hợp Nhất",
|
| 537 |
+
se: "Totalt antal sammanfogade PR",
|
| 538 |
+
he: "סך כל ה־PRs ששולבו",
|
| 539 |
+
fil: "Kabuuang mga PR na Pinagsama",
|
| 540 |
+
th: "PR ที่ถูก Merged แล้วทั้งหมด",
|
| 541 |
+
sr: "Укупно спојених PR-ова",
|
| 542 |
+
"sr-latn": "Ukupno spojenih PR-ova",
|
| 543 |
+
no: "Totalt antall sammenslåtte PR",
|
| 544 |
+
},
|
| 545 |
+
"statcard.prs-merged-percentage": {
|
| 546 |
+
en: "Merged PRs Percentage",
|
| 547 |
+
ar: "نسبة طلبات السحب المُدمجة",
|
| 548 |
+
az: "Birləşdirilmiş PR-ların Faizi",
|
| 549 |
+
ca: "Percentatge de PRs fusionats",
|
| 550 |
+
cn: "被合并的 PR 占比",
|
| 551 |
+
"zh-tw": "合併的 PR 百分比",
|
| 552 |
+
cs: "Sloučené PRs v procentech",
|
| 553 |
+
de: "Zusammengeführte PRs in Prozent",
|
| 554 |
+
sw: "Asilimia ya PRs zilizounganishwa",
|
| 555 |
+
ur: "ضم کیے گئے پی آرز کی شرح",
|
| 556 |
+
bg: "Процент сляти заявки за изтегляния",
|
| 557 |
+
bn: "PR একত্রীকরণের শতাংশ",
|
| 558 |
+
es: "Porcentaje de PR fusionados",
|
| 559 |
+
fa: "درصد درخواستهای ادغامشده",
|
| 560 |
+
fi: "Yhdistettyjen PR:ien prosentti",
|
| 561 |
+
fr: "Pourcentage de PR fusionnés",
|
| 562 |
+
hi: "मर्ज किए गए PRs प्रतिशत",
|
| 563 |
+
sa: "विलीन-पीआर प्रतिशतम्",
|
| 564 |
+
hu: "Egyesített PR-k százaléka",
|
| 565 |
+
it: "Percentuale di PR uniti",
|
| 566 |
+
ja: "マージされた PR の割合",
|
| 567 |
+
kr: "병합된 PR의 비율",
|
| 568 |
+
nl: "Percentage samengevoegde PR's",
|
| 569 |
+
"pt-pt": "Percentagem de PRs Fundidos",
|
| 570 |
+
"pt-br": "Porcentagem de PRs Integrados",
|
| 571 |
+
np: "PR मर्ज गरिएको प्रतिशत",
|
| 572 |
+
el: "Ποσοστό Συγχωνευμένων PR",
|
| 573 |
+
ro: "Procentaj PR-uri Fuzionate",
|
| 574 |
+
ru: "Процент объединённых запросов",
|
| 575 |
+
"uk-ua": "Відсоток об'єднаних запитів",
|
| 576 |
+
id: "Persentase PR Digabungkan",
|
| 577 |
+
my: "PR များကို ပေါင်းစည်းခဲ့သော ရာခိုင်နှုန်း",
|
| 578 |
+
ta: "இணைக்கப்பட்ட PRகள் சதவீதம்",
|
| 579 |
+
sk: "Percento zlúčených PR",
|
| 580 |
+
tr: "Birleştirilmiş PR Yüzdesi",
|
| 581 |
+
pl: "Procent połączonych PR",
|
| 582 |
+
uz: "Birlangan PR-lar foizi",
|
| 583 |
+
vi: "Tỷ Lệ PR Đã Hợp Nhất",
|
| 584 |
+
se: "Procent av sammanfogade PR",
|
| 585 |
+
he: "אחוז ה־PRs ששולבו",
|
| 586 |
+
fil: "Porsyento ng mga PR na Pinagsama",
|
| 587 |
+
th: "เปอร์เซ็นต์ PR ที่ถูก Merged แล้วทั้งหมด",
|
| 588 |
+
sr: "Проценат спојених PR-ова",
|
| 589 |
+
"sr-latn": "Procenat spojenih PR-ova",
|
| 590 |
+
no: "Prosentandel sammenslåtte PR",
|
| 591 |
+
},
|
| 592 |
+
};
|
| 593 |
+
};
|
| 594 |
+
|
| 595 |
+
const repoCardLocales = {
|
| 596 |
+
"repocard.template": {
|
| 597 |
+
en: "Template",
|
| 598 |
+
ar: "قالب",
|
| 599 |
+
az: "Şablon",
|
| 600 |
+
bg: "Шаблон",
|
| 601 |
+
bn: "টেমপ্লেট",
|
| 602 |
+
ca: "Plantilla",
|
| 603 |
+
cn: "模板",
|
| 604 |
+
"zh-tw": "模板",
|
| 605 |
+
cs: "Šablona",
|
| 606 |
+
de: "Vorlage",
|
| 607 |
+
sw: "Kigezo",
|
| 608 |
+
ur: "سانچہ",
|
| 609 |
+
es: "Plantilla",
|
| 610 |
+
fa: "الگو",
|
| 611 |
+
fi: "Malli",
|
| 612 |
+
fr: "Modèle",
|
| 613 |
+
hi: "खाका",
|
| 614 |
+
sa: "प्रारूपम्",
|
| 615 |
+
hu: "Sablon",
|
| 616 |
+
it: "Template",
|
| 617 |
+
ja: "テンプレート",
|
| 618 |
+
kr: "템플릿",
|
| 619 |
+
nl: "Sjabloon",
|
| 620 |
+
"pt-pt": "Modelo",
|
| 621 |
+
"pt-br": "Modelo",
|
| 622 |
+
np: "टेम्पलेट",
|
| 623 |
+
el: "Πρότυπο",
|
| 624 |
+
ro: "Șablon",
|
| 625 |
+
ru: "Шаблон",
|
| 626 |
+
"uk-ua": "Шаблон",
|
| 627 |
+
id: "Pola",
|
| 628 |
+
ml: "ടെംപ്ലേറ്റ്",
|
| 629 |
+
my: "ပုံစံ",
|
| 630 |
+
ta: `டெம்ப்ளேட்`,
|
| 631 |
+
sk: "Šablóna",
|
| 632 |
+
tr: "Şablon",
|
| 633 |
+
pl: "Szablony",
|
| 634 |
+
uz: "Shablon",
|
| 635 |
+
vi: "Mẫu",
|
| 636 |
+
se: "Mall",
|
| 637 |
+
he: "תבנית",
|
| 638 |
+
fil: "Suleras",
|
| 639 |
+
th: "เทมเพลต",
|
| 640 |
+
sr: "Шаблон",
|
| 641 |
+
"sr-latn": "Šablon",
|
| 642 |
+
no: "Mal",
|
| 643 |
+
},
|
| 644 |
+
"repocard.archived": {
|
| 645 |
+
en: "Archived",
|
| 646 |
+
ar: "مُؤرشف",
|
| 647 |
+
az: "Arxiv",
|
| 648 |
+
bg: "Архивирани",
|
| 649 |
+
bn: "আর্কাইভড",
|
| 650 |
+
ca: "Arxivats",
|
| 651 |
+
cn: "已归档",
|
| 652 |
+
"zh-tw": "已封存",
|
| 653 |
+
cs: "Archivováno",
|
| 654 |
+
de: "Archiviert",
|
| 655 |
+
sw: "Hifadhiwa kwenye kumbukumbu",
|
| 656 |
+
ur: "محفوظ شدہ",
|
| 657 |
+
es: "Archivados",
|
| 658 |
+
fa: "بایگانیشده",
|
| 659 |
+
fi: "Arkistoitu",
|
| 660 |
+
fr: "Archivé",
|
| 661 |
+
hi: "संग्रहीत",
|
| 662 |
+
sa: "संगृहीतम्",
|
| 663 |
+
hu: "Archivált",
|
| 664 |
+
it: "Archiviata",
|
| 665 |
+
ja: "アーカイブ済み",
|
| 666 |
+
kr: "보관됨",
|
| 667 |
+
nl: "Gearchiveerd",
|
| 668 |
+
"pt-pt": "Arquivados",
|
| 669 |
+
"pt-br": "Arquivados",
|
| 670 |
+
np: "अभिलेख राखियो",
|
| 671 |
+
el: "Αρχειοθετημένα",
|
| 672 |
+
ro: "Arhivat",
|
| 673 |
+
ru: "Архивирован",
|
| 674 |
+
"uk-ua": "Архивований",
|
| 675 |
+
id: "Arsip",
|
| 676 |
+
ml: "ശേഖരിച്ചത്",
|
| 677 |
+
my: "သိုလှောင်ပြီး",
|
| 678 |
+
ta: `காப்பகப்படுத்தப்பட்டது`,
|
| 679 |
+
sk: "Archivované",
|
| 680 |
+
tr: "Arşiv",
|
| 681 |
+
pl: "Zarchiwizowano",
|
| 682 |
+
uz: "Arxivlangan",
|
| 683 |
+
vi: "Đã Lưu Trữ",
|
| 684 |
+
se: "Arkiverade",
|
| 685 |
+
he: "גנוז",
|
| 686 |
+
fil: "Naka-arkibo",
|
| 687 |
+
th: "เก็บถาวร",
|
| 688 |
+
sr: "Архивирано",
|
| 689 |
+
"sr-latn": "Arhivirano",
|
| 690 |
+
no: "Arkivert",
|
| 691 |
+
},
|
| 692 |
+
};
|
| 693 |
+
|
| 694 |
+
const langCardLocales = {
|
| 695 |
+
"langcard.title": {
|
| 696 |
+
en: "Most Used Languages",
|
| 697 |
+
ar: "أكثر اللغات استخدامًا",
|
| 698 |
+
az: "Ən Çox İstifadə Olunan Dillər",
|
| 699 |
+
ca: "Llenguatges més utilitzats",
|
| 700 |
+
cn: "最常用的语言",
|
| 701 |
+
"zh-tw": "最常用的語言",
|
| 702 |
+
cs: "Nejpoužívanější jazyky",
|
| 703 |
+
de: "Meist verwendete Sprachen",
|
| 704 |
+
bg: "Най-използвани езици",
|
| 705 |
+
bn: "সর্বাধিক ব্যবহৃত ভাষা সমূহ",
|
| 706 |
+
sw: "Lugha zilizotumika zaidi",
|
| 707 |
+
ur: "سب سے زیادہ استعمال شدہ زبانیں",
|
| 708 |
+
es: "Lenguajes más usados",
|
| 709 |
+
fa: "زبانهای پرکاربرد",
|
| 710 |
+
fi: "Käytetyimmät kielet",
|
| 711 |
+
fr: "Langages les plus utilisés",
|
| 712 |
+
hi: "सर्वाधिक प्रयुक्त भाषा",
|
| 713 |
+
sa: "सर्वाधिक-प्रयुक्ताः भाषाः",
|
| 714 |
+
hu: "Leggyakrabban használt nyelvek",
|
| 715 |
+
it: "Linguaggi più utilizzati",
|
| 716 |
+
ja: "最もよく使っている言語",
|
| 717 |
+
kr: "가장 많이 사용된 언어",
|
| 718 |
+
nl: "Meest gebruikte talen",
|
| 719 |
+
"pt-pt": "Linguagens mais usadas",
|
| 720 |
+
"pt-br": "Linguagens mais usadas",
|
| 721 |
+
np: "अधिक प्रयोग गरिएको भाषाहरू",
|
| 722 |
+
el: "Οι περισσότερο χρησιμοποιούμενες γλώσσες",
|
| 723 |
+
ro: "Cele Mai Folosite Limbaje",
|
| 724 |
+
ru: "Наиболее используемые языки",
|
| 725 |
+
"uk-ua": "Найбільш використовувані мови",
|
| 726 |
+
id: "Bahasa Yang Paling Banyak Digunakan",
|
| 727 |
+
ml: "കൂടുതൽ ഉപയോഗിച്ച ഭാഷകൾ",
|
| 728 |
+
my: "အများဆုံးအသုံးပြုသောဘာသာစကားများ",
|
| 729 |
+
ta: `அதிகம் பயன்படுத்தப்படும் மொழிகள்`,
|
| 730 |
+
sk: "Najviac používané jazyky",
|
| 731 |
+
tr: "En Çok Kullanılan Diller",
|
| 732 |
+
pl: "Najczęściej używane języki",
|
| 733 |
+
uz: "Eng koʻp ishlatiladigan tillar",
|
| 734 |
+
vi: "Ngôn Ngữ Thường Sử Dụng",
|
| 735 |
+
se: "Mest använda språken",
|
| 736 |
+
he: "השפות הכי משומשות",
|
| 737 |
+
fil: "Mga Pinakamadalas na Ginagamit na Wika",
|
| 738 |
+
th: "ภาษาที่ใช้บ่อยที่สุด",
|
| 739 |
+
sr: "Најкоришћенији језици",
|
| 740 |
+
"sr-latn": "Najkorišćeniji jezici",
|
| 741 |
+
no: "Mest brukte språk",
|
| 742 |
+
},
|
| 743 |
+
"langcard.nodata": {
|
| 744 |
+
en: "No languages data.",
|
| 745 |
+
ar: "لا توجد بيانات للغات.",
|
| 746 |
+
az: "Dil məlumatı yoxdur.",
|
| 747 |
+
ca: "Sense dades d'idiomes",
|
| 748 |
+
cn: "没有语言数据。",
|
| 749 |
+
"zh-tw": "沒有語言資料。",
|
| 750 |
+
cs: "Žádné jazykové údaje.",
|
| 751 |
+
de: "Keine Sprachdaten.",
|
| 752 |
+
bg: "Няма данни за езици",
|
| 753 |
+
bn: "কোন ভাষার ডেটা নেই।",
|
| 754 |
+
sw: "Hakuna kumbukumbu ya lugha zozote",
|
| 755 |
+
ur: "کوئی زبان کا ڈیٹا نہیں۔",
|
| 756 |
+
es: "Sin datos de idiomas.",
|
| 757 |
+
fa: "دادهای برای زبانها وجود ندارد.",
|
| 758 |
+
fi: "Ei kielitietoja.",
|
| 759 |
+
fr: "Aucune donnée sur les langues.",
|
| 760 |
+
hi: "कोई भाषा डेटा नहीं",
|
| 761 |
+
sa: "भाषा-विवरणं नास्ति।",
|
| 762 |
+
hu: "Nincsenek nyelvi adatok.",
|
| 763 |
+
it: "Nessun dato sulle lingue.",
|
| 764 |
+
ja: "言語データがありません。",
|
| 765 |
+
kr: "언어 데이터가 없습니다.",
|
| 766 |
+
nl: "Ingen sprogdata.",
|
| 767 |
+
"pt-pt": "Sem dados de linguagens.",
|
| 768 |
+
"pt-br": "Sem dados de linguagens.",
|
| 769 |
+
np: "कुनै भाषा डाटा छैन।",
|
| 770 |
+
el: "Δεν υπάρχουν δεδομένα γλωσσών.",
|
| 771 |
+
ro: "Lipsesc date despre limbă.",
|
| 772 |
+
ru: "Нет данных о языках.",
|
| 773 |
+
"uk-ua": "Немає даних про мови.",
|
| 774 |
+
id: "Tidak ada data bahasa.",
|
| 775 |
+
ml: "ഭാഷാ ഡാറ്റയില്ല.",
|
| 776 |
+
my: "ဒေတာ မရှိပါ။",
|
| 777 |
+
ta: `மொழி தரவு இல்லை.`,
|
| 778 |
+
sk: "Žiadne údaje o jazykoch.",
|
| 779 |
+
tr: "Dil verisi yok.",
|
| 780 |
+
pl: "Brak danych dotyczących języków.",
|
| 781 |
+
uz: "Til haqida ma'lumot yo'q.",
|
| 782 |
+
vi: "Không có dữ liệu ngôn ngữ.",
|
| 783 |
+
se: "Inga språkdata.",
|
| 784 |
+
he: "אין נתוני שפות",
|
| 785 |
+
fil: "Walang datos ng lenggwahe.",
|
| 786 |
+
th: "ไม่มีข้อมูลภาษา",
|
| 787 |
+
sr: "Нема података о језицима.",
|
| 788 |
+
"sr-latn": "Nema podataka o jezicima.",
|
| 789 |
+
no: "Ingen språkdata.",
|
| 790 |
+
},
|
| 791 |
+
};
|
| 792 |
+
|
| 793 |
+
const wakatimeCardLocales = {
|
| 794 |
+
"wakatimecard.title": {
|
| 795 |
+
en: "WakaTime Stats",
|
| 796 |
+
ar: "إحصائيات واكا تايم",
|
| 797 |
+
az: "WakaTime Statistikası",
|
| 798 |
+
ca: "Estadístiques de WakaTime",
|
| 799 |
+
cn: "WakaTime 周统计",
|
| 800 |
+
"zh-tw": "WakaTime 周統計",
|
| 801 |
+
cs: "Statistiky WakaTime",
|
| 802 |
+
de: "WakaTime Status",
|
| 803 |
+
sw: "Takwimu ya WakaTime",
|
| 804 |
+
ur: "وکاٹائم کے اعداد و شمار",
|
| 805 |
+
bg: "WakaTime статистика",
|
| 806 |
+
bn: "WakaTime স্ট্যাটাস",
|
| 807 |
+
es: "Estadísticas de WakaTime",
|
| 808 |
+
fa: "آمار WakaTime",
|
| 809 |
+
fi: "WakaTime-tilastot",
|
| 810 |
+
fr: "Statistiques de WakaTime",
|
| 811 |
+
hi: "वाकाटाइम आँकड़े",
|
| 812 |
+
sa: "WakaTime सांख्यिकी",
|
| 813 |
+
hu: "WakaTime statisztika",
|
| 814 |
+
it: "Statistiche WakaTime",
|
| 815 |
+
ja: "WakaTime ワカタイム統計",
|
| 816 |
+
kr: "WakaTime 주간 통계",
|
| 817 |
+
nl: "WakaTime-statistieken",
|
| 818 |
+
"pt-pt": "Estatísticas WakaTime",
|
| 819 |
+
"pt-br": "Estatísticas WakaTime",
|
| 820 |
+
np: "WakaTime तथ्या .्क",
|
| 821 |
+
el: "Στατιστικά WakaTime",
|
| 822 |
+
ro: "Statistici WakaTime",
|
| 823 |
+
ru: "Статистика WakaTime",
|
| 824 |
+
"uk-ua": "Статистика WakaTime",
|
| 825 |
+
id: "Status WakaTime",
|
| 826 |
+
ml: "വാകടൈം സ്ഥിതിവിവരക്കണക്കുകൾ",
|
| 827 |
+
my: "WakaTime အချက်အလက်များ",
|
| 828 |
+
ta: `WakaTime புள்ளிவிவரங்கள்`,
|
| 829 |
+
sk: "WakaTime štatistika",
|
| 830 |
+
tr: "WakaTime İstatistikler",
|
| 831 |
+
pl: "Statystyki WakaTime",
|
| 832 |
+
uz: "WakaTime statistikasi",
|
| 833 |
+
vi: "Thống Kê WakaTime",
|
| 834 |
+
se: "WakaTime statistik",
|
| 835 |
+
he: "סטטיסטיקות WakaTime",
|
| 836 |
+
fil: "Mga Estadistika ng WakaTime",
|
| 837 |
+
th: "สถิติ WakaTime",
|
| 838 |
+
sr: "WakaTime статистика",
|
| 839 |
+
"sr-latn": "WakaTime statistika",
|
| 840 |
+
no: "WakaTime-statistikk",
|
| 841 |
+
},
|
| 842 |
+
"wakatimecard.lastyear": {
|
| 843 |
+
en: "last year",
|
| 844 |
+
ar: "العام الماضي",
|
| 845 |
+
az: "Ötən il",
|
| 846 |
+
ca: "L'any passat",
|
| 847 |
+
cn: "去年",
|
| 848 |
+
"zh-tw": "去年",
|
| 849 |
+
cs: "Minulý rok",
|
| 850 |
+
de: "Letztes Jahr",
|
| 851 |
+
sw: "Mwaka uliopita",
|
| 852 |
+
ur: "پچھلا سال",
|
| 853 |
+
bg: "миналата год.",
|
| 854 |
+
bn: "গত বছর",
|
| 855 |
+
es: "El año pasado",
|
| 856 |
+
fa: "سال گذشته",
|
| 857 |
+
fi: "Viime vuosi",
|
| 858 |
+
fr: "L'année dernière",
|
| 859 |
+
hi: "पिछले साल",
|
| 860 |
+
sa: "गतवर्षे",
|
| 861 |
+
hu: "Tavaly",
|
| 862 |
+
it: "L'anno scorso",
|
| 863 |
+
ja: "昨年",
|
| 864 |
+
kr: "작년",
|
| 865 |
+
nl: "Vorig jaar",
|
| 866 |
+
"pt-pt": "Ano passado",
|
| 867 |
+
"pt-br": "Ano passado",
|
| 868 |
+
np: "गत वर्ष",
|
| 869 |
+
el: "Πέρυσι",
|
| 870 |
+
ro: "Anul trecut",
|
| 871 |
+
ru: "За прошлый год",
|
| 872 |
+
"uk-ua": "За минулий рік",
|
| 873 |
+
id: "Tahun lalu",
|
| 874 |
+
ml: "കഴിഞ്ഞ വർഷം",
|
| 875 |
+
my: "မနှစ်က",
|
| 876 |
+
ta: `கடந்த ஆண்டு`,
|
| 877 |
+
sk: "Minulý rok",
|
| 878 |
+
tr: "Geçen yıl",
|
| 879 |
+
pl: "W zeszłym roku",
|
| 880 |
+
uz: "O'tgan yil",
|
| 881 |
+
vi: "Năm ngoái",
|
| 882 |
+
se: "Förra året",
|
| 883 |
+
he: "שנה שעברה",
|
| 884 |
+
fil: "Nakaraang Taon",
|
| 885 |
+
th: "ปีที่แล้ว",
|
| 886 |
+
sr: "Прошла год.",
|
| 887 |
+
"sr-latn": "Prošla god.",
|
| 888 |
+
no: "I fjor",
|
| 889 |
+
},
|
| 890 |
+
"wakatimecard.last7days": {
|
| 891 |
+
en: "last 7 days",
|
| 892 |
+
ar: "آخر 7 أيام",
|
| 893 |
+
az: "Son 7 gün",
|
| 894 |
+
ca: "Ultims 7 dies",
|
| 895 |
+
cn: "最近 7 天",
|
| 896 |
+
"zh-tw": "最近 7 天",
|
| 897 |
+
cs: "Posledních 7 dní",
|
| 898 |
+
de: "Letzte 7 Tage",
|
| 899 |
+
sw: "Siku 7 zilizopita",
|
| 900 |
+
ur: "پچھلے 7 دن",
|
| 901 |
+
bg: "последните 7 дни",
|
| 902 |
+
bn: "গত ৭ দিন",
|
| 903 |
+
es: "Últimos 7 días",
|
| 904 |
+
fa: "هفت روز گذشته",
|
| 905 |
+
fi: "Viimeiset 7 päivää",
|
| 906 |
+
fr: "7 derniers jours",
|
| 907 |
+
hi: "पिछले 7 दिन",
|
| 908 |
+
sa: "विगतसप्तदिनेषु",
|
| 909 |
+
hu: "Elmúlt 7 nap",
|
| 910 |
+
it: "Ultimi 7 giorni",
|
| 911 |
+
ja: "過去 7 日間",
|
| 912 |
+
kr: "지난 7 일",
|
| 913 |
+
nl: "Afgelopen 7 dagen",
|
| 914 |
+
"pt-pt": "Últimos 7 dias",
|
| 915 |
+
"pt-br": "Últimos 7 dias",
|
| 916 |
+
np: "गत ७ दिन",
|
| 917 |
+
el: "Τελευταίες 7 ημέρες",
|
| 918 |
+
ro: "Ultimele 7 zile",
|
| 919 |
+
ru: "Последние 7 дней",
|
| 920 |
+
"uk-ua": "Останні 7 днів",
|
| 921 |
+
id: "7 hari terakhir",
|
| 922 |
+
ml: "കഴിഞ്ഞ 7 ദിവസം",
|
| 923 |
+
my: "7 ရက်အတွင်း",
|
| 924 |
+
ta: `கடந்த 7 நாட்கள்`,
|
| 925 |
+
sk: "Posledných 7 dní",
|
| 926 |
+
tr: "Son 7 gün",
|
| 927 |
+
pl: "Ostatnie 7 dni",
|
| 928 |
+
uz: "O'tgan 7 kun",
|
| 929 |
+
vi: "7 ngày qua",
|
| 930 |
+
se: "Senaste 7 dagarna",
|
| 931 |
+
he: "ב־7 הימים האחרונים",
|
| 932 |
+
fil: "Huling 7 Araw",
|
| 933 |
+
th: "7 วันที่ผ่านมา",
|
| 934 |
+
sr: "Претходних 7 дана",
|
| 935 |
+
"sr-latn": "Prethodnih 7 dana",
|
| 936 |
+
no: "Siste 7 dager",
|
| 937 |
+
},
|
| 938 |
+
"wakatimecard.notpublic": {
|
| 939 |
+
en: "WakaTime user profile not public",
|
| 940 |
+
ar: "ملف مستخدم واكا تايم شخصي",
|
| 941 |
+
az: "WakaTime istifadəçi profili ictimai deyil",
|
| 942 |
+
ca: "Perfil d'usuari de WakaTime no públic",
|
| 943 |
+
cn: "WakaTime 用户个人资料未公开",
|
| 944 |
+
"zh-tw": "WakaTime 使用者個人資料未公開",
|
| 945 |
+
cs: "Profil uživatele WakaTime není veřejný",
|
| 946 |
+
de: "WakaTime-Benutzerprofil nicht öffentlich",
|
| 947 |
+
sw: "Maelezo ya mtumizi wa WakaTime si ya watu wote(umma)",
|
| 948 |
+
ur: "وکاٹائم صارف کا پروفائل عوامی نہیں",
|
| 949 |
+
bg: "Потребителски профил в WakaTime не е общодостъпен",
|
| 950 |
+
bn: "WakaTime ব্যবহারকারীর প্রোফাইল প্রকাশ্য নয়",
|
| 951 |
+
es: "Perfil de usuario de WakaTime no público",
|
| 952 |
+
fa: "پروفایل کاربری WakaTime عمومی نیست",
|
| 953 |
+
fi: "WakaTime-käyttäjäprofiili ei ole julkinen",
|
| 954 |
+
fr: "Profil utilisateur WakaTime non public",
|
| 955 |
+
hi: "WakaTime उपयोगकर्ता प्रोफ़ाइल सार्वजनिक नहीं है",
|
| 956 |
+
sa: "WakaTime उपयोगकर्ता-प्रोफ़ाइल सार्वजनिकं नास्ति",
|
| 957 |
+
hu: "A WakaTime felhasználói profilja nem nyilvános",
|
| 958 |
+
it: "Profilo utente WakaTime non pubblico",
|
| 959 |
+
ja: "WakaTime ユーザープロファイルは公開されていません",
|
| 960 |
+
kr: "WakaTime 사용자 프로필이 공개되지 않았습니다",
|
| 961 |
+
nl: "WakaTime gebruikersprofiel niet openbaar",
|
| 962 |
+
"pt-pt": "Perfil de utilizador WakaTime não público",
|
| 963 |
+
"pt-br": "Perfil de usuário WakaTime não público",
|
| 964 |
+
np: "WakaTime प्रयोगकर्ता प्रोफाइल सार्वजनिक छैन",
|
| 965 |
+
el: "Το προφίλ χρήστη WakaTime δεν είναι δημόσιο",
|
| 966 |
+
ro: "Profilul utilizatorului de Wakatime nu este public",
|
| 967 |
+
ru: "Профиль пользователя WakaTime не общедоступный",
|
| 968 |
+
"uk-ua": "Профіль користувача WakaTime не публічний",
|
| 969 |
+
id: "Profil pengguna WakaTime tidak publik",
|
| 970 |
+
ml: "WakaTime ഉപയോക്തൃ പ്രൊഫൈൽ പൊതുവായി പ്രസിദ്ധീകരിക്കപ്പെടാത്തതാണ്",
|
| 971 |
+
my: "Public Profile မဟုတ်ပါ။",
|
| 972 |
+
ta: `WakaTime பயனர் சுயவிவரம் பொதுவில் இல்லை.`,
|
| 973 |
+
sk: "Profil používateľa WakaTime nie je verejný",
|
| 974 |
+
tr: "WakaTime kullanıcı profili herkese açık değil",
|
| 975 |
+
pl: "Profil użytkownika WakaTime nie jest publiczny",
|
| 976 |
+
uz: "WakaTime foydalanuvchi profili ochiq emas",
|
| 977 |
+
vi: "Hồ sơ người dùng WakaTime không công khai",
|
| 978 |
+
se: "WakaTime användarprofil inte offentlig",
|
| 979 |
+
he: "פרופיל משתמש WakaTime לא פומבי",
|
| 980 |
+
fil: "Hindi pampubliko ang profile ng gumagamit ng WakaTime",
|
| 981 |
+
th: "โปรไฟล์ผู้ใช้ WakaTime ไม่ได้เป็นสาธารณะ",
|
| 982 |
+
sr: "WakaTime профил корисника није јаван",
|
| 983 |
+
"sr-latn": "WakaTime profil korisnika nije javan",
|
| 984 |
+
no: "WakaTime brukerprofil ikke offentlig",
|
| 985 |
+
},
|
| 986 |
+
"wakatimecard.nocodedetails": {
|
| 987 |
+
en: "User doesn't publicly share detailed code statistics",
|
| 988 |
+
ar: "المستخدم لا يشارك المعلومات التفصيلية",
|
| 989 |
+
az: "İstifadəçi kod statistikalarını ictimai şəkildə paylaşmır",
|
| 990 |
+
ca: "L'usuari no comparteix dades públiques del seu codi",
|
| 991 |
+
cn: "用户不公开分享详细的代码统计信息",
|
| 992 |
+
"zh-tw": "使用者不公開分享詳細的程式碼統計資訊",
|
| 993 |
+
cs: "Uživatel nesdílí podrobné statistiky kódu",
|
| 994 |
+
de: "Benutzer teilt keine detaillierten Code-Statistiken",
|
| 995 |
+
sw: "Mtumizi hagawi kila kitu au takwimu na umma",
|
| 996 |
+
ur: "صارف عوامی طور پر تفصیلی کوڈ کے اعداد و شمار شیئر نہیں کرتا",
|
| 997 |
+
bg: "Потребителят не споделя подробна статистика за код",
|
| 998 |
+
bn: "ব্যবহারকারী বিস্তারিত কোড পরিসংখ্যান প্রকাশ করেন না",
|
| 999 |
+
es: "El usuario no comparte públicamente estadísticas detalladas de código",
|
| 1000 |
+
fa: "کاربر آمار کد تفصیلی را بهصورت عمومی به اشتراک نمیگذارد",
|
| 1001 |
+
fi: "Käyttäjä ei jaa julkisesti tarkkoja kooditilastoja",
|
| 1002 |
+
fr: "L'utilisateur ne partage pas publiquement de statistiques de code détaillées",
|
| 1003 |
+
hi: "उपयोगकर्ता विस्तृत कोड आँकड़े सार्वजनिक रूप से साझा नहीं करता है",
|
| 1004 |
+
sa: "उपयोगकर्ता विस्तृत-कोड-सांख्यिकीं सार्वजनिकरूपेण न दर्शयति",
|
| 1005 |
+
hu: "A felhasználó nem osztja meg nyilvánosan a részletes kódstatisztikákat",
|
| 1006 |
+
it: "L'utente non condivide pubblicamente statistiche dettagliate sul codice",
|
| 1007 |
+
ja: "ユーザーは詳細なコード統計を公開しません",
|
| 1008 |
+
kr: "사용자는 자세한 코드 통계를 공개하지 않습니다",
|
| 1009 |
+
nl: "Gebruiker deelt geen gedetailleerde code-statistieken",
|
| 1010 |
+
"pt-pt":
|
| 1011 |
+
"O utilizador não partilha publicamente estatísticas detalhadas de código",
|
| 1012 |
+
"pt-br":
|
| 1013 |
+
"O usuário não compartilha publicamente estatísticas detalhadas de código",
|
| 1014 |
+
np: "प्रयोगकर्ता सार्वजनिक रूपमा विस्तृत कोड तथ्याङ्क स���झा गर्दैन",
|
| 1015 |
+
el: "Ο χρήστης δεν δημοσιεύει δημόσια λεπτομερείς στατιστικές κώδικα",
|
| 1016 |
+
ro: "Utilizatorul nu își publică statisticile detaliate ale codului",
|
| 1017 |
+
ru: "Пользователь не делится подробной статистикой кода",
|
| 1018 |
+
"uk-ua": "Користувач не публікує детальну статистику коду",
|
| 1019 |
+
id: "Pengguna tidak membagikan statistik kode terperinci secara publik",
|
| 1020 |
+
ml: "ഉപയോക്താവ് പൊതുവായി വിശദീകരിച്ച കോഡ് സ്റ്റാറ്റിസ്റ്റിക്സ് പങ്കിടുന്നില്ല",
|
| 1021 |
+
my: "အသုံးပြုသူသည် အသေးစိတ် ကုဒ် စာရင်းအင်းများကို အများသို့ မမျှဝေပါ။",
|
| 1022 |
+
ta: `பயனர் விரிவான குறியீட்டு புள்ளிவிவரங்களைப் பொதுவில் பகிர்வதில்லை.`,
|
| 1023 |
+
sk: "Používateľ neposkytuje verejne podrobné štatistiky kódu",
|
| 1024 |
+
tr: "Kullanıcı ayrıntılı kod istatistiklerini herkese açık olarak paylaşmıyor",
|
| 1025 |
+
pl: "Użytkownik nie udostępnia publicznie szczegółowych statystyk kodu",
|
| 1026 |
+
uz: "Foydalanuvchi umumiy ko`d statistikasini ochiq ravishda almashmaydi",
|
| 1027 |
+
vi: "Người dùng không chia sẻ thống kê mã chi tiết công khai",
|
| 1028 |
+
se: "Användaren delar inte offentligt detaljerad kodstatistik",
|
| 1029 |
+
he: "משתמש לא מפרסם פומבית סטטיסטיקות קוד מפורטות",
|
| 1030 |
+
fil: "Hindi ibinabahagi ng gumagamit ang detalyadong estadistika ng code nang pampubliko",
|
| 1031 |
+
th: "ผู้ใช้ไม่ได้แชร์สถิติโค้ดแบบสาธารณะ",
|
| 1032 |
+
sr: "Корисник не дели јавно детаљну статистику кода",
|
| 1033 |
+
"sr-latn": "Korisnik ne deli javno detaljnu statistiku koda",
|
| 1034 |
+
no: "Brukeren deler ikke detaljert kodestatistikk offentlig",
|
| 1035 |
+
},
|
| 1036 |
+
"wakatimecard.nocodingactivity": {
|
| 1037 |
+
en: "No coding activity this week",
|
| 1038 |
+
ar: "لا يوجد نشاط برمجي لهذا الأسبوع",
|
| 1039 |
+
az: "Bu həftə heç bir kodlaşdırma fəaliyyəti olmayıb",
|
| 1040 |
+
ca: "No hi ha activitat de codificació aquesta setmana",
|
| 1041 |
+
cn: "本周没有编程活动",
|
| 1042 |
+
"zh-tw": "本周沒有編程活動",
|
| 1043 |
+
cs: "Tento týden žádná aktivita v kódování",
|
| 1044 |
+
de: "Keine Aktivitäten in dieser Woche",
|
| 1045 |
+
sw: "Hakuna matukio yoyote ya kusimba wiki hii",
|
| 1046 |
+
ur: "اس ہفتے کوئی کوڈنگ سرگرمی نہیں",
|
| 1047 |
+
bg: "Няма активност при кодирането тази седмица",
|
| 1048 |
+
bn: "এই সপ্তাহে কোন কোডিং অ্যাক্টিভিটি নেই",
|
| 1049 |
+
es: "No hay actividad de codificación esta semana",
|
| 1050 |
+
fa: "فعالیت کدنویسی در این هفته وجود ندارد",
|
| 1051 |
+
fi: "Ei koodaustoimintaa tällä viikolla",
|
| 1052 |
+
fr: "Aucune activité de codage cette semaine",
|
| 1053 |
+
hi: "इस सप्ताह कोई कोडिंग गतिविधि नहीं ",
|
| 1054 |
+
sa: "अस्मिन् सप्ताहे कोडिङ्-कार्यं नास्ति",
|
| 1055 |
+
hu: "Nem volt aktivitás ezen a héten",
|
| 1056 |
+
it: "Nessuna attività in questa settimana",
|
| 1057 |
+
ja: "今週のコーディング活動はありません",
|
| 1058 |
+
kr: "이번 주 작업내역 없음",
|
| 1059 |
+
nl: "Geen programmeeractiviteit deze week",
|
| 1060 |
+
"pt-pt": "Sem atividade esta semana",
|
| 1061 |
+
"pt-br": "Nenhuma atividade de codificação esta semana",
|
| 1062 |
+
np: "यस हप्ता कुनै कोडिंग गतिविधि छैन",
|
| 1063 |
+
el: "Δεν υπάρχει δραστηριότητα κώδικα γι' αυτή την εβδομάδα",
|
| 1064 |
+
ro: "Nicio activitate de programare săptămâna aceasta",
|
| 1065 |
+
ru: "На этой неделе не было активности",
|
| 1066 |
+
"uk-ua": "Цього тижня не було активності",
|
| 1067 |
+
id: "Tidak ada aktivitas perkodingan minggu ini",
|
| 1068 |
+
ml: "ഈ ആഴ്ച കോഡിംഗ് പ്രവർത്തനങ്ങളൊന്നുമില്ല",
|
| 1069 |
+
my: "ဒီအပတ်မှာ ကုဒ်ရေးခြင်း မရှိပါ။",
|
| 1070 |
+
ta: `இந்த வாரம் குறியீட்டு செயல்பாடு இல்லை.`,
|
| 1071 |
+
sk: "Žiadna kódovacia aktivita tento týždeň",
|
| 1072 |
+
tr: "Bu hafta herhangi bir kod yazma aktivitesi olmadı",
|
| 1073 |
+
pl: "Brak aktywności w tym tygodniu",
|
| 1074 |
+
uz: "Bu hafta faol bo'lmadi",
|
| 1075 |
+
vi: "Không Có Hoạt Động Trong Tuần Này",
|
| 1076 |
+
se: "Ingen aktivitet denna vecka",
|
| 1077 |
+
he: "אין פעילות תכנותית השבוע",
|
| 1078 |
+
fil: "Walang aktibidad sa pag-code ngayong linggo",
|
| 1079 |
+
th: "ไม่มีกิจกรรมการเขียนโค้ดในสัปดาห์นี้",
|
| 1080 |
+
sr: "Током ове недеље није било никаквих активности",
|
| 1081 |
+
"sr-latn": "Tokom ove nedelje nije bilo nikakvih aktivnosti",
|
| 1082 |
+
no: "Ingen kodeaktivitet denne uken",
|
| 1083 |
+
},
|
| 1084 |
+
};
|
| 1085 |
+
|
| 1086 |
+
const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
|
| 1087 |
+
|
| 1088 |
+
/**
|
| 1089 |
+
* Checks whether the locale is available or not.
|
| 1090 |
+
*
|
| 1091 |
+
* @param {string} locale The locale to check.
|
| 1092 |
+
* @returns {boolean} Boolean specifying whether the locale is available or not.
|
| 1093 |
+
*/
|
| 1094 |
+
const isLocaleAvailable = (locale) => {
|
| 1095 |
+
return availableLocales.includes(locale.toLowerCase());
|
| 1096 |
+
};
|
| 1097 |
+
|
| 1098 |
+
export {
|
| 1099 |
+
availableLocales,
|
| 1100 |
+
isLocaleAvailable,
|
| 1101 |
+
langCardLocales,
|
| 1102 |
+
repoCardLocales,
|
| 1103 |
+
statCardLocales,
|
| 1104 |
+
wakatimeCardLocales,
|
| 1105 |
+
};
|