diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 71b71275a2510..fa7228a6af615 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,6 +1,6 @@
# These are supported funding model platforms
-github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+github: [anuraghazra] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0eb02215f0312..89df34e6006e6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,17 +20,6 @@ jobs:
with:
node-version: "12.x"
- - name: Cache node modules
- uses: actions/cache@v2
- env:
- cache-name: cache-node-modules
- with:
- path: ~/.npm
- key:
- ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-npm-cache-
-
- name: Install & Test
run: |
npm install
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 04528df1566a8..35bfdbd1f2b2b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,6 +17,15 @@ Pull requests are the best way to propose changes. We actively welcome your pull
1. If you've changed APIs, update the documentation.
1. Issue that pull request!
+## Under the hood of github-readme-stats
+
+Interested in diving deeper into understanding how github-readme-stats works?
+
+[Bohdan](https://github.com/Bogdan-Lyashenko) wrote an amazing in-depth post about it, check it out:
+
+**[Under the hood of github-readme-stats project](https://codecrumbs.io/library/github-readme-stats)**
+
+
## Local Development
To run & test github-readme-stats you need to follow few simple steps :-
@@ -71,7 +80,7 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
**Q:** How to count private stats?
-> **Ans:** We can only count private commits & we cannot access any other private info of any users, so it's not possible. only way is to deploy on your own instance & use your own PAT (Personal Access Token)
+> **Ans:** We can only count public commits & we cannot access any other private info of any users, so it's not possible. The only way to count your personal private stats is to deploy on your own instance & use your own PAT (Personal Access Token)
### Bug Reports
diff --git a/api/index.js b/api/index.js
index 235065bbde86b..c23177f7382df 100644
--- a/api/index.js
+++ b/api/index.js
@@ -17,6 +17,7 @@ module.exports = async (req, res) => {
hide,
hide_title,
hide_border,
+ card_width,
hide_rank,
show_icons,
count_private,
@@ -25,6 +26,7 @@ module.exports = async (req, res) => {
title_color,
icon_color,
text_color,
+ text_bold,
bg_color,
theme,
cache_seconds,
@@ -35,8 +37,6 @@ module.exports = async (req, res) => {
border_color,
role,
} = req.query;
- let stats;
-
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
@@ -48,11 +48,11 @@ module.exports = async (req, res) => {
}
try {
- stats = await fetchStats(
+ const stats = await fetchStats(
username,
+ parseArray(role),
parseBoolean(count_private),
parseBoolean(include_all_commits),
- parseArray(role),
);
const cacheSeconds = clampValue(
@@ -69,12 +69,14 @@ module.exports = async (req, res) => {
show_icons: parseBoolean(show_icons),
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
+ card_width: parseInt(card_width, 10),
hide_rank: parseBoolean(hide_rank),
include_all_commits: parseBoolean(include_all_commits),
line_height,
title_color,
icon_color,
text_color,
+ text_bold: parseBoolean(text_bold),
bg_color,
theme,
custom_title,
diff --git a/api/pin.js b/api/pin.js
index 7fad6c09f3746..1df7fd0780f8b 100644
--- a/api/pin.js
+++ b/api/pin.js
@@ -27,8 +27,6 @@ module.exports = async (req, res) => {
border_color,
} = req.query;
- let repoData;
-
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
@@ -40,7 +38,7 @@ module.exports = async (req, res) => {
}
try {
- repoData = await fetchRepo(username, repo);
+ const repoData = await fetchRepo(username, repo);
let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
@@ -53,7 +51,7 @@ module.exports = async (req, res) => {
and if both are zero we are not showing the stats
so we can just make the cache longer, since there is no need to frequent updates
*/
- const stars = repoData.stargazers.totalCount;
+ const stars = repoData.starCount;
const forks = repoData.forkCount;
const isBothOver1K = stars > 1000 && forks > 1000;
const isBothUnder1 = stars < 1 && forks < 1;
diff --git a/api/top-langs.js b/api/top-langs.js
index 6d0e089d1c384..ce2dfeb5f2e62 100644
--- a/api/top-langs.js
+++ b/api/top-langs.js
@@ -32,8 +32,6 @@ module.exports = async (req, res) => {
border_color,
role,
} = req.query;
- let topLangs;
-
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
@@ -45,10 +43,10 @@ module.exports = async (req, res) => {
}
try {
- topLangs = await fetchTopLanguages(
+ const topLangs = await fetchTopLanguages(
username,
- parseArray(exclude_repo),
parseArray(role),
+ parseArray(exclude_repo),
parseArray(hide),
);
diff --git a/api/wakatime.js b/api/wakatime.js
index 4bb8fca5d16c0..d585c4a025f09 100644
--- a/api/wakatime.js
+++ b/api/wakatime.js
@@ -3,9 +3,10 @@ const {
renderError,
parseBoolean,
clampValue,
+ parseArray,
CONSTANTS,
- isLocaleAvailable,
} = require("../src/common/utils");
+const { isLocaleAvailable } = require("../src/translations");
const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher");
const wakatimeCard = require("../src/cards/wakatime-card");
@@ -26,6 +27,7 @@ module.exports = async (req, res) => {
locale,
layout,
langs_count,
+ hide,
api_domain,
range,
border_radius,
@@ -58,6 +60,7 @@ module.exports = async (req, res) => {
custom_title,
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
+ hide: parseArray(hide),
line_height,
title_color,
icon_color,
diff --git a/docs/readme_cn.md b/docs/readme_cn.md
index 60a7d26c036f4..36a5ff9359454 100644
--- a/docs/readme_cn.md
+++ b/docs/readme_cn.md
@@ -325,7 +325,7 @@ _注意:热门语言并不表示我的技能水平或类似的水平,它是
1. 选择 `Import Git Repository`

1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
- 
+ 
1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了!
diff --git a/docs/readme_es.md b/docs/readme_es.md
index ae0cc830f0e8e..b35e89c12c323 100644
--- a/docs/readme_es.md
+++ b/docs/readme_es.md
@@ -64,7 +64,7 @@
- [Tarjeta de estadísticas de GitHub](#tarjeta-de-estadísticas-de-github)
- [Pins adicionales de GitHub](#pines-adicionales-de-github)
-- [Top Languages Card](#tarjeta-de-lenguajes-principales)
+- [Tarjeta de Lenguajes Principales](#tarjeta-de-lenguajes-principales)
- [Wakatime Week Stats](#estadísticas-de-la-semana-de-wakatime)
- [Temas](#temas)
- [Personalización](#personalización)
diff --git a/package.json b/package.json
index 0cb50dc167e6e..345388f9714b1 100644
--- a/package.json
+++ b/package.json
@@ -16,11 +16,14 @@
"@actions/github": "^4.0.0",
"@testing-library/dom": "^7.20.0",
"@testing-library/jest-dom": "^5.11.0",
- "axios": "^0.19.2",
+ "axios": "^0.24.0",
"axios-mock-adapter": "^1.18.1",
+ "color-contrast-checker": "^2.1.0",
"css-to-object": "^1.1.0",
+ "hjson": "^3.2.2",
"husky": "^4.2.5",
"jest": "^26.1.0",
+ "lodash.snakecase": "^4.1.1",
"parse-diff": "^0.7.0"
},
"dependencies": {
diff --git a/readme.md b/readme.md
index 068e39789a71c..8b0c593792e98 100644
--- a/readme.md
+++ b/readme.md
@@ -83,6 +83,11 @@ Your small help goes a long way. :heart:
- [Wakatime Week Stats](#wakatime-week-stats)
- [Themes](#themes)
- [Customization](#customization)
+ - [Common Options](#common-options)
+ - [Stats Card Exclusive Options](#stats-card-exclusive-options)
+ - [Repo Card Exclusive Options](#repo-card-exclusive-options)
+ - [Language Card Exclusive Options](#language-card-exclusive-options)
+ - [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
# GitHub Stats Card
@@ -113,7 +118,7 @@ To hide any specific stats, you can pass a query parameter `?hide=` with comma-s
You can add the count of all your private contributions to the total commits count by using the query parameter `?count_private=true`.
-_Note: If you are deploying this project yourself, the private contributions will be counted by default otherwise you need to chose to share your private contribution counts._
+_Note: If you are deploying this project yourself, the private contributions will be counted by default. Otherwise, you need to choose to share your private contribution counts._
> Options: `&count_private=true`
@@ -133,7 +138,7 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
With inbuilt themes, you can customize the look of the card without doing any [manual customization](#customization).
-Use `?theme=THEME_NAME` parameter like so :-
+Use `&theme=THEME_NAME` parameter like so :-
```md

@@ -178,12 +183,14 @@ You can provide multiple comma-separated values in bg_color option to render a g
- `hide` - Hides the specified items from stats _(Comma-separated values)_
- `hide_title` - _(boolean)_
+- `card_width` - Set the card's width manually _(number)_
- `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width
- `show_icons` - _(boolean)_
- `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_
- `count_private` - Count private commits _(boolean)_
- `line_height` - Sets the line-height between text _(number)_
- `custom_title` - Sets a custom title for the card
+- `text_bold` - Use bold text _(boolean)_
- `disable_animations` - Disables all animations in the card _(boolean)_
#### Repo Card Exclusive Options:
@@ -207,6 +214,7 @@ You can provide multiple comma-separated values in bg_color option to render a g
#### Wakatime Card Exclusive Options:
+- `hide` - Hide the languages specified from the card _(Comma-separated values)_
- `hide_title` - _(boolean)_
- `line_height` - Sets the line-height between text _(number)_
- `hide_progress` - Hides the progress bar and percentage _(boolean)_
@@ -405,6 +413,7 @@ NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we

1. Sign into GitHub and allow access to all repositories, if prompted
1. Fork this repo
+1. After forking the repo, open the [`vercel.json`](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json#L5) file and change the `maxDuration` field to `10`
1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
1. Select `Import Project`

@@ -437,3 +446,4 @@ Thanks! :heart:
Contributions are welcome! <3
Made with :heart: and JavaScript.
+
diff --git a/scripts/generate-theme-doc.js b/scripts/generate-theme-doc.js
index 806b2cc3fc2b4..fcb3c5eee705c 100644
--- a/scripts/generate-theme-doc.js
+++ b/scripts/generate-theme-doc.js
@@ -12,7 +12,7 @@ const THEME_TEMPLATE = `## Available Themes
-With inbuilt themes you can customize the look of the card without doing any manual customization.
+With inbuilt themes, you can customize the look of the card without doing any manual customization.
Use \`?theme=THEME_NAME\` parameter like so :-
@@ -43,7 +43,7 @@ ${REPO_CARD_LINKS_FLAG}
[add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
-Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
+Want to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
`;
const createRepoMdLink = (theme) => {
diff --git a/scripts/preview-theme.js b/scripts/preview-theme.js
index e809777b16e6a..ca9effae79cc0 100644
--- a/scripts/preview-theme.js
+++ b/scripts/preview-theme.js
@@ -1,8 +1,16 @@
const core = require("@actions/core");
const github = require("@actions/github");
const parse = require("parse-diff");
+const Hjson = require("hjson");
+const snakeCase = require("lodash.snakecase");
+const ColorContrastChecker = require("color-contrast-checker");
+
require("dotenv").config();
+const OWNER = "anuraghazra";
+const REPO = "github-readme-stats";
+const COMMENT_TITLE = "Automated Theme Preview";
+
function getPrNumber() {
const pullRequest = github.context.payload.pull_request;
if (!pullRequest) {
@@ -12,17 +20,72 @@ function getPrNumber() {
return pullRequest.number;
}
-const themeContribGuidelines = `
- \r> Hi thanks for the theme contribution, please read our theme contribution guidelines
+function findCommentPredicate(inputs, comment) {
+ return (
+ (inputs.commentAuthor && comment.user
+ ? comment.user.login === inputs.commentAuthor
+ : true) &&
+ (inputs.bodyIncludes && comment.body
+ ? comment.body.includes(inputs.bodyIncludes)
+ : true)
+ );
+}
+
+async function findComment(octokit, issueNumber) {
+ const parameters = {
+ owner: OWNER,
+ repo: REPO,
+ issue_number: issueNumber,
+ };
+ const inputs = {
+ commentAuthor: OWNER,
+ bodyIncludes: COMMENT_TITLE,
+ };
+
+ for await (const { data: comments } of octokit.paginate.iterator(
+ octokit.rest.issues.listComments,
+ parameters,
+ )) {
+ // Search each page for the comment
+ const comment = comments.find((comment) =>
+ findCommentPredicate(inputs, comment),
+ );
+ if (comment) return comment;
+ }
+}
+
+async function upsertComment(octokit, props) {
+ if (props.comment_id !== undefined) {
+ await octokit.issues.updateComment(props);
+ } else {
+ await octokit.issues.createComment(props);
+ }
+}
+
+function getWebAimLink(color1, color2) {
+ return `https://webaim.org/resources/contrastchecker/?fcolor=${color1}&bcolor=${color2}`;
+}
- \r> We are currently only accepting color combinations from any vscode theme or which has good color combination to minimize bloating the themes collection.
+function getGrsLink(colors) {
+ const url = `https://github-readme-stats.vercel.app/api?username=anuraghazra`;
+ const colorString = Object.keys(colors)
+ .map((colorKey) => `${colorKey}=${colors[colorKey]}`)
+ .join("&");
- \r> Also note that if this theme is exclusively for your personal use then instead of adding it to our theme collection you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization)
- \r> Read our [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md) for more info
+ return `${url}&${colorString}&show_icons=true`;
+}
+
+const themeContribGuidelines = `
+ \rHi, thanks for the theme contribution, please read our theme [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution).
+ \rWe are currently only accepting color combinations from any VSCode theme or themes which have good color combination to minimize bloating the themes collection.
+
+ \r> Also note that if this theme is exclusively for your personal use, then instead of adding it to our theme collection you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization)
`;
async function run() {
try {
+ const ccc = new ColorContrastChecker();
+ const warnings = [];
const token = core.getInput("token");
const octokit = github.getOctokit(token || process.env.PERSONAL_TOKEN);
const pullRequestId = getPrNumber();
@@ -32,65 +95,90 @@ async function run() {
return;
}
- let res = await octokit.pulls.get({
- owner: "anuraghazra",
- repo: "github-readme-stats",
+ const res = await octokit.pulls.get({
+ owner: OWNER,
+ repo: REPO,
pull_number: pullRequestId,
mediaType: {
format: "diff",
},
});
+ const comment = await findComment(octokit, pullRequestId);
- let diff = parse(res.data);
- let colorStrings = diff
+ const diff = parse(res.data);
+ const content = diff
.find((file) => file.to === "themes/index.js")
.chunks[0].changes.filter((c) => c.type === "add")
.map((c) => c.content.replace("+", ""))
.join("");
- let matches = colorStrings.match(/(title_color:.*bg_color.*\")/);
- let colors = matches && matches[0].split(",");
+ const themeObject = Hjson.parse(content);
+ const themeName = Object.keys(themeObject)[0];
+ const colors = themeObject[themeName];
+
+ if (themeName !== snakeCase(themeName)) {
+ warnings.push("Theme name isn't in snake_case");
+ }
if (!colors) {
- await octokit.issues.createComment({
- owner: "anuraghazra",
- repo: "github-readme-stats",
+ await upsertComment({
+ comment_id: comment?.id,
+ owner: OWNER,
+ repo: REPO,
+ issue_number: pullRequestId,
body: `
- \rTheme preview (bot)
+ \r**${COMMENT_TITLE}**
\rCannot create theme preview
${themeContribGuidelines}
`,
- issue_number: pullRequestId,
});
return;
}
- colors = colors.map((color) =>
- color.replace(/.*\:\s/, "").replace(/\"/g, ""),
- );
- const titleColor = colors[0];
- const iconColor = colors[1];
- const textColor = colors[2];
- const bgColor = colors[3];
- const url = `https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=${titleColor}&icon_color=${iconColor}&text_color=${textColor}&bg_color=${bgColor}&show_icons=true`;
+ const titleColor = colors.title_color;
+ const iconColor = colors.icon_color;
+ const textColor = colors.text_color;
+ const bgColor = colors.bg_color;
+ const url = getGrsLink(colors);
+
+ const colorPairs = {
+ title_color: [titleColor, bgColor],
+ icon_color: [iconColor, bgColor],
+ text_color: [textColor, bgColor],
+ };
+
+ // check color contrast
+ Object.keys(colorPairs).forEach((key) => {
+ const color1 = colorPairs[key][0];
+ const color2 = colorPairs[key][1];
+ if (!ccc.isLevelAA(`#${color1}`, `#${color2}`)) {
+ const permalink = getWebAimLink(color1, color2);
+ warnings.push(
+ `\`${key}\` does not passes [AA contrast ratio](${permalink})`,
+ );
+ }
+ });
- await octokit.issues.createComment({
- owner: "anuraghazra",
- repo: "github-readme-stats",
+ await upsertComment(octokit, {
+ comment_id: comment?.id,
+ issue_number: pullRequestId,
+ owner: OWNER,
+ repo: REPO,
body: `
- \rTheme preview (bot)
+ \r**${COMMENT_TITLE}**
+ \r${warnings.map((warning) => `- :warning: ${warning}\n`).join("")}
+
\ntitle_color: #${titleColor}
| icon_color: #${iconColor}
| text_color: #${textColor}
| bg_color: #${bgColor}
- \rLink: ${url}
+ \r[Preview Link](${url})
\r[](${url})
${themeContribGuidelines}
`,
- issue_number: pullRequestId,
});
} catch (error) {
console.log(error);
diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js
index 9a21fcbdeb358..5295b174735a5 100644
--- a/src/cards/repo-card.js
+++ b/src/cards/repo-card.js
@@ -1,25 +1,79 @@
-const toEmoji = require("emoji-name-map");
const {
kFormatter,
encodeHTML,
getCardColors,
flexLayout,
wrapTextMultiline,
+ measureText,
+ parseEmojis,
} = require("../common/utils");
const I18n = require("../common/I18n");
const Card = require("../common/Card");
const icons = require("../common/icons");
const { repoCardLocales } = require("../translations");
+/**
+ * @param {string} label
+ * @param {string} textColor
+ * @returns {string}
+ */
+const getBadgeSVG = (label, textColor) => `
+
+
+
+ ${label}
+
+
+`;
+
+/**
+ * @param {string} langName
+ * @param {string} langColor
+ * @returns {string}
+ */
+const createLanguageNode = (langName, langColor) => {
+ return `
+
+
+ ${langName}
+
+ `;
+};
+
+const ICON_SIZE = 16;
+const iconWithLabel = (icon, label, testid) => {
+ if (label <= 0) return "";
+ const iconSvg = `
+
+ `;
+ const text = `${label}`;
+ return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
+};
+
const renderRepoCard = (repo, options = {}) => {
const {
name,
nameWithOwner,
description,
primaryLanguage,
- stargazers,
isArchived,
isTemplate,
+ starCount,
forkCount,
} = repo;
const {
@@ -35,22 +89,17 @@ const renderRepoCard = (repo, options = {}) => {
locale,
} = options;
+ const lineHeight = 10;
const header = show_owner ? nameWithOwner : name;
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
- const shiftText = langName.length > 15 ? 0 : 30;
-
- let desc = description || "No description provided";
-
- // parse emojis to unicode
- desc = desc.replace(/:\w+:/gm, (emoji) => {
- return toEmoji.get(emoji) || "";
- });
-
+ const desc = parseEmojis(description || "No description provided");
const multiLineDescription = wrapTextMultiline(desc);
const descriptionLines = multiLineDescription.length;
- const lineHeight = 10;
+ const descriptionSvg = multiLineDescription
+ .map((line) => `${encodeHTML(line)}`)
+ .join("");
const height =
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
@@ -61,13 +110,7 @@ const renderRepoCard = (repo, options = {}) => {
});
// returns theme based colors with proper overrides and defaults
- const {
- titleColor,
- textColor,
- iconColor,
- bgColor,
- borderColor,
- } = getCardColors({
+ const colors = getCardColors({
title_color,
icon_color,
text_color,
@@ -76,74 +119,41 @@ const renderRepoCard = (repo, options = {}) => {
theme,
});
- const totalStars = kFormatter(stargazers.totalCount);
- const totalForks = kFormatter(forkCount);
-
- const getBadgeSVG = (label) => `
-
-
-
- ${label}
-
-
- `;
-
const svgLanguage = primaryLanguage
- ? `
-
-
- ${langName}
-
- `
+ ? createLanguageNode(langName, langColor)
: "";
- const iconWithLabel = (icon, label, testid) => {
- return `
-
- ${label}
- `;
- };
- const svgStars =
- stargazers.totalCount > 0 &&
- iconWithLabel(icons.star, totalStars, "stargazers");
- const svgForks =
- forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
+ const totalStars = kFormatter(starCount);
+ const totalForks = kFormatter(forkCount);
+ const svgStars = iconWithLabel(icons.star, totalStars, "stargazers");
+ const svgForks = iconWithLabel(icons.fork, totalForks, "forkcount");
const starAndForkCount = flexLayout({
- items: [svgStars, svgForks],
- gap: 65,
+ items: [svgLanguage, svgStars, svgForks],
+ sizes: [
+ measureText(langName, 12),
+ ICON_SIZE + measureText(`${totalStars}`, 12),
+ ICON_SIZE + measureText(`${totalForks}`, 12),
+ ],
+ gap: 25,
}).join("");
const card = new Card({
- defaultTitle: header,
+ defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
titlePrefixIcon: icons.contribs,
width: 400,
height,
border_radius,
- colors: {
- titleColor,
- textColor,
- iconColor,
- bgColor,
- borderColor,
- },
+ colors,
});
card.disableAnimations();
card.setHideBorder(hide_border);
card.setHideTitle(false);
card.setCSS(`
- .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
- .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
- .icon { fill: ${iconColor} }
+ .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
+ .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
+ .icon { fill: ${colors.iconColor} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
`);
@@ -151,27 +161,18 @@ const renderRepoCard = (repo, options = {}) => {
return card.render(`
${
isTemplate
- ? getBadgeSVG(i18n.t("repocard.template"))
+ ? getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
: isArchived
- ? getBadgeSVG(i18n.t("repocard.archived"))
+ ? getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
: ""
}
- ${multiLineDescription
- .map((line) => `${encodeHTML(line)}`)
- .join("")}
+ ${descriptionSvg}
-
- ${svgLanguage}
-
-
- ${starAndForkCount}
-
+
+ ${starAndForkCount}
`);
};
diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js
index 99582e0abe2a8..fee830dc74e00 100644
--- a/src/cards/stats-card.js
+++ b/src/cards/stats-card.js
@@ -19,6 +19,7 @@ const createTextNode = ({
index,
showIcons,
shiftValuePos,
+ bold,
}) => {
const kValue = kFormatter(value);
const staggerDelay = (index + 3) * 150;
@@ -34,18 +35,18 @@ const createTextNode = ({
return `
${iconSvg}
- ${label}:
- ${label}:
+ ${kValue}
`;
};
-const renderStatsCard = (stats = {}, options = { hide: [] }) => {
+const renderStatsCard = (stats = {}, options = { hide: []}) => {
const {
name,
totalStars,
@@ -60,12 +61,14 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
show_icons = false,
hide_title = false,
hide_border = false,
+ card_width,
hide_rank = false,
include_all_commits = false,
line_height = 25,
title_color,
icon_color,
text_color,
+ text_bold = true,
bg_color,
theme = "default",
custom_title,
@@ -78,20 +81,15 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const lheight = parseInt(line_height, 10);
// returns theme based colors with proper overrides and defaults
- const {
- titleColor,
- textColor,
- iconColor,
- bgColor,
- borderColor,
- } = getCardColors({
- title_color,
- icon_color,
- text_color,
- bg_color,
- border_color,
- theme,
- });
+ const { titleColor, textColor, iconColor, bgColor, borderColor } =
+ getCardColors({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ border_color,
+ theme,
+ });
const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase())
? ""
@@ -147,6 +145,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
"id",
"my",
"pl",
+ "de",
+ "nl",
];
const isLongLocale = longLocales.includes(locale) === true;
@@ -160,7 +160,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
index,
showIcons: show_icons,
shiftValuePos:
- (!include_all_commits ? 50 : 20) + (isLongLocale ? 50 : 0),
+ (!include_all_commits ? 50 : 35) + (isLongLocale ? 50 : 0),
+ bold: text_bold,
}),
);
@@ -171,26 +172,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
hide_rank ? 0 : 150,
);
- // Conditionally rendered elements
- const rankCircle = hide_rank
- ? ""
- : `
-
-
-
-
- ${rank.level}
-
-
- `;
-
// the better user's score the the rank will be closer to zero so
// subtracting 100 to get the progress in 100%
const progress = 100 - rank.score;
@@ -206,13 +187,20 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
return measureText(custom_title ? custom_title : i18n.t("statcard.title"));
};
- const width = hide_rank
- ? clampValue(
- 50 /* padding */ + calculateTextWidth() * 2,
- 270 /* min */,
- Infinity,
- )
- : 495;
+ /*
+ When hide_rank=true, the minimum card width is 270 px + the title length and padding.
+ When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
+ Numbers are picked by looking at existing dimensions on production.
+ */
+ const iconWidth = show_icons ? 16 : 0;
+ const minCardWidth = hide_rank
+ ? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity)
+ : 340 + iconWidth;
+ const defaultCardWidth = hide_rank ? 270 : 495;
+ let width = isNaN(card_width) ? defaultCardWidth : card_width;
+ if (width < minCardWidth) {
+ width = minCardWidth;
+ }
const card = new Card({
customTitle: custom_title,
@@ -235,6 +223,45 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
if (disable_animations) card.disableAnimations();
+ /**
+ * Calculates the right rank circle translation values such that the rank circle
+ * keeps respecting the padding.
+ *
+ * width > 450: The default left padding of 50 px will be used.
+ * width < 450: The left and right padding will shrink equally.
+ *
+ * @returns {number} - Rank circle translation value.
+ */
+ const calculateRankXTranslation = () => {
+ if (width < 450) {
+ return width - 95 + (45 * (450 - 340)) / 110;
+ } else {
+ return width - 95;
+ }
+ };
+
+ // Conditionally rendered elements
+ const rankCircle = hide_rank
+ ? ""
+ : `
+
+
+
+
+ ${rank.level}
+
+
+ `;
+
return card.render(`
${rankCircle}
@@ -244,7 +271,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
gap: lheight,
direction: "column",
}).join("")}
-
+
`);
};
diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js
index f281b12567239..c8109f36d6955 100644
--- a/src/cards/top-languages-card.js
+++ b/src/cards/top-languages-card.js
@@ -2,15 +2,20 @@ const Card = require("../common/Card");
const I18n = require("../common/I18n");
const { langCardLocales } = require("../translations");
const { createProgressNode } = require("../common/createProgressNode");
-const { clampValue, getCardColors, flexLayout } = require("../common/utils");
+const {
+ clampValue,
+ getCardColors,
+ flexLayout,
+ lowercaseTrim,
+ measureText,
+ chunkArray,
+} = require("../common/utils");
const DEFAULT_CARD_WIDTH = 300;
const DEFAULT_LANGS_COUNT = 5;
const DEFAULT_LANG_COLOR = "#858585";
const CARD_PADDING = 25;
-const lowercaseTrim = (name) => name.toLowerCase().trim();
-
const createProgressTextNode = ({ width, color, name, progress }) => {
const paddingRight = 95;
const progressTextX = width - paddingRight + 10;
@@ -30,12 +35,12 @@ const createProgressTextNode = ({ width, color, name, progress }) => {
`;
};
-const createCompactLangNode = ({ lang, totalSize, x, y }) => {
+const createCompactLangNode = ({ lang, totalSize }) => {
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
const color = lang.color || "#858585";
return `
-
+
${lang.name} ${percentage}%
@@ -44,25 +49,38 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
`;
};
-const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
- return langs.map((lang, index) => {
- if (index % 2 === 0) {
- return createCompactLangNode({
+const getLongestLang = (arr) =>
+ arr.reduce(
+ (savedLang, lang) =>
+ lang.name.length > savedLang.name.length ? lang : savedLang,
+ { name: "" },
+ );
+
+const createLanguageTextNode = ({ langs, totalSize }) => {
+ const longestLang = getLongestLang(langs);
+ const chunked = chunkArray(langs, langs.length / 2);
+ const layouts = chunked.map((array) => {
+ const items = array.map((lang, index) =>
+ createCompactLangNode({
lang,
- x,
- y: 12.5 * index + y,
totalSize,
index,
- });
- }
- return createCompactLangNode({
- lang,
- x: 150,
- y: 12.5 + 12.5 * index,
- totalSize,
- index,
- });
+ }),
+ );
+ return flexLayout({
+ items,
+ gap: 25,
+ direction: "column",
+ }).join("");
});
+
+ const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
+ const minGap = 150;
+ const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
+ return flexLayout({
+ items: layouts,
+ gap: maxGap < minGap ? minGap : maxGap,
+ }).join("");
};
/**
@@ -129,12 +147,14 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
${compactProgressBar}
- ${createLanguageTextNode({
- x: 0,
- y: 25,
- langs,
- totalSize: totalLanguageSize,
- }).join("")}
+
+
+ ${createLanguageTextNode({
+ langs,
+ totalSize: totalLanguageSize,
+ width,
+ })}
+
`;
};
diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js
index 5e12cf3910d19..905e40a08fefc 100644
--- a/src/cards/wakatime-card.js
+++ b/src/cards/wakatime-card.js
@@ -4,7 +4,12 @@ const { getStyles } = require("../getStyles");
const { wakatimeCardLocales } = require("../translations");
const languageColors = require("../common/languageColors.json");
const { createProgressNode } = require("../common/createProgressNode");
-const { clampValue, getCardColors, flexLayout } = require("../common/utils");
+const {
+ clampValue,
+ getCardColors,
+ flexLayout,
+ lowercaseTrim,
+} = require("../common/utils");
const noCodingActivityNode = ({ color, text }) => {
return `
@@ -61,34 +66,47 @@ const createTextNode = ({
const cardProgress = hideProgress
? null
: createProgressNode({
- x: 110,
- y: 4,
- progress: percent,
- color: progressBarColor,
- width: 220,
- name: label,
- progressBarBackgroundColor,
- });
+ x: 110,
+ y: 4,
+ progress: percent,
+ color: progressBarColor,
+ width: 220,
+ name: label,
+ progressBarBackgroundColor,
+ });
return `
- ${label}:
+ ${label}:
${value}
${cardProgress}
`;
};
+const recalculatePercentages = (languages) => {
+ // recalculating percentages so that,
+ // compact layout's progress bar does not break when hiding languages
+ const totalSum = languages.reduce(
+ (totalSum, language) => totalSum + language.percent,
+ 0,
+ );
+ const weight = (100 / totalSum).toFixed(2);
+ languages.forEach((language) => {
+ language.percent = (language.percent * weight).toFixed(2);
+ });
+};
+
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
- const { languages } = stats;
+ let { languages } = stats;
const {
hide_title = false,
hide_border = false,
+ hide,
line_height = 25,
title_color,
icon_color,
@@ -104,6 +122,15 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
border_color,
} = options;
+ const shouldHideLangs = Array.isArray(hide) && hide.length > 0;
+ if (shouldHideLangs && languages !== undefined) {
+ const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang)));
+ languages = languages.filter(
+ (lang) => !languagesToHide.has(lowercaseTrim(lang.name)),
+ );
+ recalculatePercentages(languages);
+ }
+
const i18n = new I18n({
locale,
translations: wakatimeCardLocales,
@@ -111,7 +138,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
const lheight = parseInt(line_height, 10);
- langsCount = clampValue(parseInt(langs_count), 1, langs_count);
+ const langsCount = clampValue(parseInt(langs_count), 1, langs_count);
// returns theme based colors with proper overrides and defaults
const {
@@ -131,8 +158,8 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
const filteredLanguages = languages
? languages
- .filter((language) => language.hours || language.minutes)
- .slice(0, langsCount)
+ .filter((language) => language.hours || language.minutes)
+ .slice(0, langsCount)
: [];
// Calculate the card height depending on how many items there are
@@ -186,17 +213,16 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
${compactProgressBar}
${createLanguageTextNode({
- x: 0,
- y: 25,
- langs: filteredLanguages,
- totalSize: 100,
- }).join("")}
+ x: 0,
+ y: 25,
+ langs: filteredLanguages,
+ totalSize: 100,
+ }).join("")}
`;
} else {
finalLayout = flexLayout({
items: filteredLanguages.length
- ? filteredLanguages
- .map((language) => {
+ ? filteredLanguages.map((language) => {
return createTextNode({
id: language.name,
label: language.name,
@@ -208,11 +234,11 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
});
})
: [
- noCodingActivityNode({
- color: textColor,
- text: i18n.t("wakatimecard.nocodingactivity"),
- }),
- ],
+ noCodingActivityNode({
+ color: textColor,
+ text: i18n.t("wakatimecard.nocodingactivity"),
+ }),
+ ],
gap: lheight,
direction: "column",
}).join("");
diff --git a/src/common/Card.js b/src/common/Card.js
index 520560f94d24d..fd1fa77d09c61 100644
--- a/src/common/Card.js
+++ b/src/common/Card.js
@@ -2,6 +2,16 @@ const { getAnimations } = require("../getStyles");
const { flexLayout, encodeHTML } = require("../common/utils");
class Card {
+ /**
+ * @param {object} args
+ * @param {number?=} args.width
+ * @param {number?=} args.height
+ * @param {number?=} args.border_radius
+ * @param {string?=} args.customTitle
+ * @param {string?=} args.defaultTitle
+ * @param {string?=} args.titlePrefixIcon
+ * @param {ReturnType?=} args.colors
+ */
constructor({
width = 100,
height = 100,
@@ -38,14 +48,23 @@ class Card {
this.animations = false;
}
+ /**
+ * @param {string} value
+ */
setCSS(value) {
this.css = value;
}
+ /**
+ * @param {boolean} value
+ */
setHideBorder(value) {
this.hideBorder = value;
}
+ /**
+ * @param {boolean} value
+ */
setHideTitle(value) {
this.hideTitle = value;
if (value) {
@@ -53,6 +72,9 @@ class Card {
}
}
+ /**
+ * @param {string} text
+ */
setTitle(text) {
this.title = text;
}
@@ -114,6 +136,9 @@ class Card {
: "";
}
+ /**
+ * @param {string} body
+ */
render(body) {
return `