Compare commits

..
31 changed files with 1863 additions and 592 deletions
+2 -2
View File
@@ -19,7 +19,7 @@ updates:
interval: weekly interval: weekly
open-pull-requests-limit: 10 open-pull-requests-limit: 10
commit-message: commit-message:
prefix: "ci(deps)" prefix: "build(deps)"
prefix-development: "ci(deps-dev)" prefix-development: "build(deps-dev)"
reviewers: reviewers:
- "qwerty541" - "qwerty541"
+44 -86
View File
@@ -1,95 +1,53 @@
themes: themes: themes/index.js
- changed-files: doc-translation: docs/*
- any-glob-to-any-file:
- themes/index.js
doc-translation:
- changed-files:
- any-glob-to-any-file:
- docs/*
card-i18n: card-i18n:
- changed-files: - src/translations.js
- any-glob-to-any-file: - src/common/I18n.js
- src/translations.js
- src/common/I18n.js
documentation: documentation:
- changed-files: - readme.md
- any-glob-to-any-file: - CONTRIBUTING.md
- readme.md - CODE_OF_CONDUCT.md
- CONTRIBUTING.md - SECURITY.md
- CODE_OF_CONDUCT.md
- SECURITY.md
dependencies: dependencies:
- changed-files: - package.json
- any-glob-to-any-file: - package-lock.json
- package.json
- package-lock.json
lang-card: lang-card:
- changed-files: - api/top-langs.js
- any-glob-to-any-file: - src/cards/top-languages-card.js
- api/top-langs.js - src/fetchers/top-languages-fetcher.js
- src/cards/top-languages-card.js - tests/fetchTopLanguages.test.js
- src/fetchers/top-languages-fetcher.js - tests/renderTopLanguagesCard.test.js
- tests/fetchTopLanguages.test.js - tests/top-langs.test.js
- tests/renderTopLanguagesCard.test.js
- tests/top-langs.test.js
repo-card: repo-card:
- changed-files: - api/pin.js
- any-glob-to-any-file: - src/cards/repo-card.js
- api/pin.js - src/fetchers/repo-fetcher.js
- src/cards/repo-card.js - tests/fetchRepo.test.js
- src/fetchers/repo-fetcher.js - tests/renderRepoCard.test.js
- tests/fetchRepo.test.js - tests/pin.test.js
- tests/renderRepoCard.test.js
- tests/pin.test.js
stats-card: stats-card:
- changed-files: - api/index.js
- any-glob-to-any-file: - src/cards/stats-card.js
- api/index.js - src/fetchers/stats-fetcher.js
- src/cards/stats-card.js - tests/fetchStats.test.js
- src/fetchers/stats-fetcher.js - tests/renderStatsCard.test.js
- tests/fetchStats.test.js - tests/api.test.js
- tests/renderStatsCard.test.js
- tests/api.test.js
wakatime-card: wakatime-card:
- changed-files: - api/wakatime.js
- any-glob-to-any-file: - src/cards/wakatime-card.js
- api/wakatime.js - src/fetchers/wakatime-fetcher.js
- src/cards/wakatime-card.js - tests/fetchWakatime.test.js
- src/fetchers/wakatime-fetcher.js - tests/renderWakatimeCard.test.js
- tests/fetchWakatime.test.js - tests/wakatime.test.js
- tests/renderWakatimeCard.test.js
- tests/wakatime.test.js
gist-card: gist-card:
- changed-files: - api/gist.js
- any-glob-to-any-file: - src/cards/gist-card.js
- api/gist.js - src/fetchers/gist-fetcher.js
- src/cards/gist-card.js - tests/fetchGist.test.js
- src/fetchers/gist-fetcher.js - tests/renderGistCard.test.js
- tests/fetchGist.test.js - tests/gist.test.js
- tests/renderGistCard.test.js ranks: src/calculateRank.js
- tests/gist.test.js
ranks:
- changed-files:
- any-glob-to-any-file:
- src/calculateRank.js
ci: ci:
- changed-files: - .github/workflows/*
- any-glob-to-any-file: - scripts/*
- .github/workflows/* infrastructure: .eslintrc.json
- scripts/*
infrastructure:
- changed-files:
- any-glob-to-any-file:
- .eslintrc.json
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
if: github.repository == 'anuraghazra/github-readme-stats' if: github.repository == 'anuraghazra/github-readme-stats'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Deployment Prep - name: Deployment Prep
run: python ./.github/workflows/deploy-prep.py run: python ./.github/workflows/deploy-prep.py
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1 - uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5.0.0
with: with:
branch: vercel branch: vercel
create_branch: true create_branch: true
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
@@ -27,10 +27,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# NOTE: Retrieve issue templates. # NOTE: Retrieve issue templates.
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run empty issues closer action - name: Run empty issues closer action
uses: rickstaa/empty-issues-closer-action@e96914613221511279ca25f50fd4acc85e331d99 # v1.1.74 uses: rickstaa/empty-issues-closer-action@e0fb1033c83cf13e0c98c2c9d0419d3040a6a1d0 # v1.1.50
env: env:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
with: with:
+2 -2
View File
@@ -29,10 +29,10 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
+1 -2
View File
@@ -21,7 +21,6 @@ jobs:
if: github.repository == 'anuraghazra/github-readme-stats' if: github.repository == 'anuraghazra/github-readme-stats'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true
+3 -4
View File
@@ -11,7 +11,6 @@ permissions: read-all
jobs: jobs:
analysis: analysis:
if: github.repository == 'anuraghazra/github-readme-stats'
name: Scorecard analysis name: Scorecard analysis
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -22,12 +21,12 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@@ -36,7 +35,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
+3 -3
View File
@@ -30,15 +30,15 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- uses: bahmutov/npm-install@e5c7e14408aa6089501de32bd16123b41738047e # v1.10.2 - uses: bahmutov/npm-install@2509f13e8485d88340a789a3f7ca11aaac47c9fc # v1.8.36
with: with:
useLockFile: false useLockFile: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Cleanup - name: Cleanup
run: | run: |
@@ -36,15 +36,15 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- uses: bahmutov/npm-install@e5c7e14408aa6089501de32bd16123b41738047e # v1.10.2 - uses: bahmutov/npm-install@2509f13e8485d88340a789a3f7ca11aaac47c9fc # v1.8.36
with: with:
useLockFile: false useLockFile: false
+3 -3
View File
@@ -18,10 +18,10 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
@@ -44,4 +44,4 @@ jobs:
npm run format:check npm run format:check
- name: Code Coverage - name: Code Coverage
uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3.1.5 uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Run top issues action - name: Run top issues action
uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101 uses: rickstaa/top-issues-action@04028f4fc0f1c217e571be72102b6ea4827b8468 # v1.3.75
env: env:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -36,10 +36,10 @@ jobs:
node-version: [18.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
@@ -53,7 +53,7 @@ jobs:
run: npm run generate-langs-json run: npm run generate-langs-json
- name: Create Pull Request if upstream language file is changed - name: Create Pull Request if upstream language file is changed
uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5 uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
with: with:
commit-message: "refactor: update languages JSON" commit-message: "refactor: update languages JSON"
branch: "update_langs/patch" branch: "update_langs/patch"
+3
View File
@@ -1,3 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test npm test
npm run lint npm run lint
npx lint-staged npx lint-staged
+32 -13
View File
@@ -8,7 +8,36 @@ import {
renderError, renderError,
} from "../src/common/utils.js"; } from "../src/common/utils.js";
import { fetchStats } from "../src/fetchers/stats-fetcher.js"; import { fetchStats } from "../src/fetchers/stats-fetcher.js";
import { isLocaleAvailable } from "../src/translations.js"; import { validateQueryStringParams } from "../src/common/validate.js";
const QUERYSTRING_PARAMS_DATA_TYPE_MAP = new Map([
["username", "string"],
["hide", "enum-array"],
["hide_title", "boolean"],
["hide_border", "boolean"],
["card_width", "number"],
["hide_rank", "boolean"],
["show_icons", "boolean"],
["include_all_commits", "boolean"],
["line_height", "number"],
["title_color", "string"],
["ring_color", "string"],
["icon_color", "string"],
["text_color", "string"],
["text_bold", "boolean"],
["bg_color", "string"],
["theme", "enum"],
["cache_seconds", "number"],
["exclude_repo", "array"],
["custom_title", "string"],
["locale", "enum"],
["disable_animations", "boolean"],
["border_radius", "number"],
["border_color", "string"],
["number_format", "enum"],
["rank_icon", "enum"],
["show", "enum-array"],
]);
export default async (req, res) => { export default async (req, res) => {
const { const {
@@ -53,19 +82,9 @@ export default async (req, res) => {
); );
} }
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError("Something went wrong", "Language not found", {
title_color,
text_color,
bg_color,
border_color,
theme,
}),
);
}
try { try {
validateQueryStringParams(req.query, QUERYSTRING_PARAMS_DATA_TYPE_MAP);
const showStats = parseArray(show); const showStats = parseArray(show);
const stats = await fetchStats( const stats = await fetchStats(
username, username,
+1303 -395
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -29,7 +29,7 @@
"generate-langs-json": "node scripts/generate-langs-json", "generate-langs-json": "node scripts/generate-langs-json",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"prepare": "husky", "prepare": "husky install",
"lint": "npx eslint --max-warnings 0 \"./src/**/*.js\" \"./scripts/**/*.js\" \"./tests/**/*.js\" \"./api/**/*.js\" \"./themes/**/*.js\"", "lint": "npx eslint --max-warnings 0 \"./src/**/*.js\" \"./scripts/**/*.js\" \"./tests/**/*.js\" \"./api/**/*.js\" \"./themes/**/*.js\"",
"bench": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.bench.config.js" "bench": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.bench.config.js"
}, },
@@ -38,27 +38,27 @@
"devDependencies": { "devDependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@testing-library/dom": "^10.1.0", "@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.1.4",
"@uppercod/css-to-object": "^1.1.1", "@uppercod/css-to-object": "^1.1.1",
"axios-mock-adapter": "^1.22.0", "axios-mock-adapter": "^1.22.0",
"color-contrast-checker": "^2.1.0", "color-contrast-checker": "^2.1.0",
"eslint": "^8.57.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.0.0",
"hjson": "^3.2.2", "hjson": "^3.2.2",
"husky": "^9.0.11", "husky": "^8.0.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-bench": "^29.7.1", "jest-bench": "^29.4.1",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lint-staged": "^15.2.2", "lint-staged": "^15.1.0",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
"parse-diff": "^0.11.1", "parse-diff": "^0.11.1",
"prettier": "^3.2.5" "prettier": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.1", "axios": "^1.6.2",
"dotenv": "^16.4.5", "dotenv": "^16.3.1",
"emoji-name-map": "^1.2.8", "emoji-name-map": "^1.2.8",
"github-username-regex": "^1.0.0", "github-username-regex": "^1.0.0",
"upgrade": "^1.1.0", "upgrade": "^1.1.0",
+2 -2
View File
@@ -297,7 +297,7 @@ You can customize the appearance of all your cards however you wish with URL par
| `bg_color` | Card's background color. | string (hex color or a gradient in the form of *angle,start,end*) | `fffefe` | | `bg_color` | Card's background color. | string (hex color or a gradient in the form of *angle,start,end*) | `fffefe` |
| `hide_border` | Hides the card's border. | boolean | `false` | | `hide_border` | Hides the card's border. | boolean | `false` |
| `theme` | Name of the theme, choose from [all available themes](themes/README.md). | enum | `default` | | `theme` | Name of the theme, choose from [all available themes](themes/README.md). | enum | `default` |
| `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` | | `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `1800` |
| `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` | | `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` |
| `border_radius` | Corner rounding on the card. | number | `4.5` | | `border_radius` | Corner rounding on the card. | number | `4.5` |
@@ -432,7 +432,7 @@ If we don't support your language, please consider contributing! You can find mo
| `custom_title` | Sets a custom title for the card. | string | `WakaTime Stats` | | `custom_title` | Sets a custom title for the card. | string | `WakaTime Stats` |
| `layout` | Switches between two available layouts `default` & `compact`. | enum | `default` | | `layout` | Switches between two available layouts `default` & `compact`. | enum | `default` |
| `langs_count` | Limits the number of languages on the card, defaults to all reported languages. | integer | `null` | | `langs_count` | Limits the number of languages on the card, defaults to all reported languages. | integer | `null` |
| `api_domain` | Sets a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) | string | `wakatime.com` | | `api_domain` | Sets a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) | string | `Waka API` |
| `display_format` | Sets the WakaTime stats display format. Choose `time` to display time-based stats or `percent` to show percentages. | enum | `time` | | `display_format` | Sets the WakaTime stats display format. Choose `time` to display time-based stats or `percent` to show percentages. | enum | `time` |
| `disable_animations` | Disables all animations in the card. | boolean | `false` | | `disable_animations` | Disables all animations in the card. | boolean | `false` |
+1 -1
View File
@@ -518,7 +518,7 @@ const renderStatsCard = (stats, options = {}) => {
if (key === "commits") { if (key === "commits") {
return `${i18n.t("statcard.commits")} ${ return `${i18n.t("statcard.commits")} ${
include_all_commits ? "" : `in ${new Date().getFullYear()}` include_all_commits ? "" : `in ${new Date().getFullYear()}`
} : ${STATS[key].value}`; } : ${totalStars}`;
} }
return `${STATS[key].label}: ${STATS[key].value}`; return `${STATS[key].label}: ${STATS[key].value}`;
}) })
+1 -1
View File
@@ -46,7 +46,7 @@ export type TopLangOptions = CommonOptions & {
hide_progress: boolean; hide_progress: boolean;
}; };
export type WakaTimeOptions = CommonOptions & { type WakaTimeOptions = CommonOptions & {
hide_title: boolean; hide_title: boolean;
hide: string[]; hide: string[];
line_height: string; line_height: string;
+8 -16
View File
@@ -39,20 +39,6 @@ const noCodingActivityNode = ({ color, text }) => {
* @typedef {import('../fetchers/types').WakaTimeLang} WakaTimeLang * @typedef {import('../fetchers/types').WakaTimeLang} WakaTimeLang
*/ */
/**
* Format language value.
*
* @param {Object} args The function arguments.
* @param {WakaTimeLang} args.lang The language object.
* @param {"time" | "percent"} args.display_format The display format of the language node.
* @returns {string} The formatted language value.
*/
const formatLanguageValue = ({ display_format, lang }) => {
return display_format === "percent"
? `${lang.percent.toFixed(2).toString()} %`
: lang.text;
};
/** /**
* Create compact WakaTime layout. * Create compact WakaTime layout.
* *
@@ -65,7 +51,10 @@ const formatLanguageValue = ({ display_format, lang }) => {
*/ */
const createCompactLangNode = ({ lang, x, y, display_format }) => { const createCompactLangNode = ({ lang, x, y, display_format }) => {
const color = languageColors[lang.name] || "#858585"; const color = languageColors[lang.name] || "#858585";
const value = formatLanguageValue({ display_format, lang }); const value =
display_format === "percent"
? `${lang.percent.toFixed(2).toString()} %`
: lang.text;
return ` return `
<g transform="translate(${x}, ${y})"> <g transform="translate(${x}, ${y})">
@@ -352,7 +341,10 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
return createTextNode({ return createTextNode({
id: language.name, id: language.name,
label: language.name, label: language.name,
value: formatLanguageValue({ display_format, lang: language }), value:
display_format === "percent"
? `${language.percent.toFixed(2).toString()} %`
: language.text,
index, index,
percent: language.percent, percent: language.percent,
// @ts-ignore // @ts-ignore
+3 -15
View File
@@ -130,8 +130,6 @@
"Easybuild": "#069406", "Easybuild": "#069406",
"Ecere Projects": "#913960", "Ecere Projects": "#913960",
"Ecmarkup": "#eb8131", "Ecmarkup": "#eb8131",
"Edge": "#0dffe0",
"EdgeQL": "#31A7FF",
"EditorConfig": "#fff1f2", "EditorConfig": "#fff1f2",
"Eiffel": "#4d6977", "Eiffel": "#4d6977",
"Elixir": "#6e4a7e", "Elixir": "#6e4a7e",
@@ -172,8 +170,8 @@
"Game Maker Language": "#71b417", "Game Maker Language": "#71b417",
"Gemfile.lock": "#701516", "Gemfile.lock": "#701516",
"Gemini": "#ff6900", "Gemini": "#ff6900",
"Genero 4gl": "#63408e", "Genero": "#63408e",
"Genero per": "#d8df39", "Genero Forms": "#d8df39",
"Genie": "#fb855d", "Genie": "#fb855d",
"Genshi": "#951531", "Genshi": "#951531",
"Gentoo Ebuild": "#9400ff", "Gentoo Ebuild": "#9400ff",
@@ -184,8 +182,6 @@
"Git Config": "#F44D27", "Git Config": "#F44D27",
"Git Revision List": "#F44D27", "Git Revision List": "#F44D27",
"Gleam": "#ffaff3", "Gleam": "#ffaff3",
"Glimmer JS": "#F5835F",
"Glimmer TS": "#3178c6",
"Glyph": "#c1ac7f", "Glyph": "#c1ac7f",
"Gnuplot": "#f0a9f0", "Gnuplot": "#f0a9f0",
"Go": "#00ADD8", "Go": "#00ADD8",
@@ -316,7 +312,6 @@
"Modelica": "#de1d31", "Modelica": "#de1d31",
"Modula-2": "#10253f", "Modula-2": "#10253f",
"Modula-3": "#223388", "Modula-3": "#223388",
"Mojo": "#ff4c1f",
"Monkey C": "#8D6747", "Monkey C": "#8D6747",
"MoonScript": "#ff4585", "MoonScript": "#ff4585",
"Motoko": "#fbb03b", "Motoko": "#fbb03b",
@@ -383,7 +378,6 @@
"PicoLisp": "#6067af", "PicoLisp": "#6067af",
"PigLatin": "#fcd7de", "PigLatin": "#fcd7de",
"Pike": "#005390", "Pike": "#005390",
"Pip Requirements": "#FFD343",
"PlantUML": "#fbbd16", "PlantUML": "#fbbd16",
"PogoScript": "#d80074", "PogoScript": "#d80074",
"Polar": "#ae81ff", "Polar": "#ae81ff",
@@ -392,7 +386,6 @@
"PostScript": "#da291c", "PostScript": "#da291c",
"PowerBuilder": "#8f0f8d", "PowerBuilder": "#8f0f8d",
"PowerShell": "#012456", "PowerShell": "#012456",
"Praat": "#c8506d",
"Prisma": "#0c344b", "Prisma": "#0c344b",
"Processing": "#0096D8", "Processing": "#0096D8",
"Procfile": "#3B2F63", "Procfile": "#3B2F63",
@@ -435,7 +428,6 @@
"Ring": "#2D54CB", "Ring": "#2D54CB",
"Riot": "#A71E49", "Riot": "#A71E49",
"RobotFramework": "#00c0b5", "RobotFramework": "#00c0b5",
"Roc": "#7c38f5",
"Roff": "#ecdebe", "Roff": "#ecdebe",
"Roff Manpage": "#ecdebe", "Roff Manpage": "#ecdebe",
"Rouge": "#cc0088", "Rouge": "#cc0088",
@@ -468,7 +460,6 @@
"Slash": "#007eff", "Slash": "#007eff",
"Slice": "#003fa2", "Slice": "#003fa2",
"Slim": "#2b2b2b", "Slim": "#2b2b2b",
"Slint": "#2379F4",
"SmPL": "#c94949", "SmPL": "#c94949",
"Smalltalk": "#596706", "Smalltalk": "#596706",
"Smarty": "#f0c040", "Smarty": "#f0c040",
@@ -487,7 +478,7 @@
"SugarSS": "#2fcc9f", "SugarSS": "#2fcc9f",
"SuperCollider": "#46390b", "SuperCollider": "#46390b",
"Svelte": "#ff3e00", "Svelte": "#ff3e00",
"Sway": "#00F58C", "Sway": "#dea584",
"Sweave": "#198ce7", "Sweave": "#198ce7",
"Swift": "#F05138", "Swift": "#F05138",
"SystemVerilog": "#DAE1C2", "SystemVerilog": "#DAE1C2",
@@ -503,12 +494,9 @@
"Tcl": "#e4cc98", "Tcl": "#e4cc98",
"TeX": "#3D6117", "TeX": "#3D6117",
"Terra": "#00004c", "Terra": "#00004c",
"Terraform Template": "#7b42bb",
"TextGrid": "#c8506d",
"TextMate Properties": "#df66e4", "TextMate Properties": "#df66e4",
"Textile": "#ffe7ac", "Textile": "#ffe7ac",
"Thrift": "#D12127", "Thrift": "#D12127",
"Toit": "#c2c9fb",
"Turing": "#cf142b", "Turing": "#cf142b",
"Twig": "#c1d026", "Twig": "#c1d026",
"TypeScript": "#3178c6", "TypeScript": "#3178c6",
+22
View File
@@ -0,0 +1,22 @@
export type DataTypes =
| "enum"
| "enum-array"
| "array"
| "string"
| "boolean"
| "number";
export function validateQueryStringParam(
expectedDataType: DateTypes,
param: string,
value: string,
): boolean;
export function validateQueryStringParams(
queryStringParams: object,
queryStringParamsDataTypeMap: Map<string, DataTypes>,
): void;
export class InvalidQueryStringParamsError extends Error {
constructor(message: string, secondaryMessage: string);
}
+128
View File
@@ -0,0 +1,128 @@
// @ts-check
import { availableLocales } from "../translations.js";
import { themes } from "../../themes/index.js";
import { parseArray } from "./utils.js";
/**
* Class for handling invalid query string param errors.
*/
export class InvalidQueryStringParamsError extends Error {
/**
* Constructor for InvalidQueryStringParamsError.
*
* @param {string} message - The error message.
* @param {string} secondaryMessage - The secondary error message.
*/
constructor(message, secondaryMessage) {
super(message);
this.secondaryMessage = secondaryMessage;
}
}
const QUERYSTRING_PARAMS_ENUM_VALUES = {
hide: ["stars", "commits", "prs", "issues", "contribs"],
theme: Object.keys(themes),
locale: availableLocales,
number_format: ["short", "long"],
rank_icon: ["github", "percentile", "default"],
show: [
"reviews",
"discussions_started",
"discussions_answered",
"prs_merged",
"prs_merged_percentage",
],
};
/**
* @typedef {import("./validate").DataTypes} DataTypes The data types.
*/
/**
* Returns the secondary error message for an invalid query string param.
*
* @param {string} param - The invalid query string param.
* @param {Map<string, DataTypes>} queryStringParamsDataTypeMap - The query string params data type map.
* @returns {string} The secondary error message.
*/
const getInvalidQueryStringParamsErrorSecondaryMessage = (
param,
queryStringParamsDataTypeMap,
) => {
const expectedDataType = queryStringParamsDataTypeMap.get(param);
if (expectedDataType === "enum" || expectedDataType === "enum-array") {
return `Expected: ${QUERYSTRING_PARAMS_ENUM_VALUES[param].join(", ")}`;
} else if (expectedDataType === "number") {
return "Expected: a number";
} else if (expectedDataType === "boolean") {
return "Expected: true or false";
} else if (expectedDataType === "array") {
return "Expected: an array";
} else if (expectedDataType === "string") {
return "Expected: a string";
} else {
throw new Error("Unexpected behavior");
}
};
/**
* Validates a query string param.
*
* @param {DataTypes} expectedDataType - The expected data type of the query string param.
* @param {string} param - The query string param.
* @param {string} value - The query string param value.
* @returns {boolean} Whether the query string param is valid.
*/
export const validateQueryStringParam = (expectedDataType, param, value) => {
if (expectedDataType === "enum") {
return QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value);
} else if (expectedDataType === "enum-array") {
return parseArray(value).every((value) =>
QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value),
);
} else if (expectedDataType === "number") {
return !isNaN(parseFloat(value));
} else if (expectedDataType === "boolean") {
return value === "true" || value === "false";
} else if (expectedDataType === "array") {
return Array.isArray(parseArray(value));
} else if (expectedDataType === "string") {
return typeof value === "string";
} else {
return false;
}
};
/**
* Validates the query string params.
* Throws an error if a query string param is invalid.
* Does not return anything.
*
* @param {object} queryStringParams - The query string params.
* @param {Map<string, DataTypes>} queryStringParamsDataTypeMap - The query string params data type map.
* @returns {void}
*/
export const validateQueryStringParams = (
queryStringParams,
queryStringParamsDataTypeMap,
) => {
for (const [param, value] of Object.entries(queryStringParams)) {
const expectedDataType = queryStringParamsDataTypeMap.get(param);
if (!expectedDataType) {
// Absence of data type means that the query string param is not supported.
// Currently we allow addition of extra query string params.
continue;
}
if (validateQueryStringParam(expectedDataType, param, value)) {
continue;
}
throw new InvalidQueryStringParamsError(
`Invalid query string param \`${param}\` value: ${value}`,
getInvalidQueryStringParamsErrorSecondaryMessage(
param,
queryStringParamsDataTypeMap,
),
);
}
};
+10 -6
View File
@@ -6,6 +6,7 @@ import { calculateRank } from "../src/calculateRank.js";
import { renderStatsCard } from "../src/cards/stats-card.js"; import { renderStatsCard } from "../src/cards/stats-card.js";
import { CONSTANTS, renderError } from "../src/common/utils.js"; import { CONSTANTS, renderError } from "../src/common/utils.js";
import { expect, it, describe, afterEach } from "@jest/globals"; import { expect, it, describe, afterEach } from "@jest/globals";
import { availableLocales } from "../src/translations.js";
const stats = { const stats = {
name: "Anurag Hazra", name: "Anurag Hazra",
@@ -140,8 +141,8 @@ describe("Test /api/", () => {
{ {
username: "anuraghazra", username: "anuraghazra",
hide: "issues,prs,contribs", hide: "issues,prs,contribs",
show_icons: true, show_icons: "true",
hide_border: true, hide_border: "true",
line_height: 100, line_height: 100,
title_color: "fff", title_color: "fff",
icon_color: "fff", icon_color: "fff",
@@ -270,8 +271,8 @@ describe("Test /api/", () => {
{ {
username: "anuraghazra", username: "anuraghazra",
hide: "issues,prs,contribs", hide: "issues,prs,contribs",
show_icons: true, show_icons: "true",
hide_border: true, hide_border: "true",
line_height: 100, line_height: 100,
title_color: "fff", title_color: "fff",
ring_color: "0000ff", ring_color: "0000ff",
@@ -318,7 +319,10 @@ describe("Test /api/", () => {
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith( expect(res.send).toBeCalledWith(
renderError("Something went wrong", "Language not found"), renderError(
"Invalid query string param `locale` value: asdf",
`Expected: ${availableLocales.join(", ")}`,
),
); );
}); });
@@ -328,7 +332,7 @@ describe("Test /api/", () => {
.reply(200, { error: "Some test error message" }); .reply(200, { error: "Some test error message" });
const { req, res } = faker( const { req, res } = faker(
{ username: "anuraghazra", include_all_commits: true }, { username: "anuraghazra", include_all_commits: "true" },
data_stats, data_stats,
); );
+8 -11
View File
@@ -14,20 +14,19 @@ import { expect, describe, beforeAll, test } from "@jest/globals";
const REPO = "curly-fiesta"; const REPO = "curly-fiesta";
const USER = "catelinemnemosyne"; const USER = "catelinemnemosyne";
const STATS_CARD_USER = "e2eninja";
const GIST_ID = "372cef55fd897b31909fdeb3a7262758"; const GIST_ID = "372cef55fd897b31909fdeb3a7262758";
const STATS_DATA = { const STATS_DATA = {
name: "E2ENinja", name: "Cateline Mnemosyne",
totalPRs: 1, totalPRs: 2,
totalReviews: 0, totalReviews: 0,
totalCommits: 3, totalCommits: 8,
totalIssues: 1, totalIssues: 1,
totalStars: 1, totalStars: 1,
contributedTo: 0, contributedTo: 1,
rank: { rank: {
level: "C", level: "C",
percentile: 98.73972605284538, percentile: 98.06929469995667,
}, },
}; };
@@ -117,17 +116,15 @@ describe("Fetch Cards", () => {
// Check if the Vercel preview instance stats card function is up and running. // Check if the Vercel preview instance stats card function is up and running.
await expect( await expect(
axios.get(`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}`), axios.get(`${VERCEL_PREVIEW_URL}/api?username=${USER}`),
).resolves.not.toThrow(); ).resolves.not.toThrow();
// Get local stats card. // Get local stats card.
const localStatsCardSVG = renderStatsCard(STATS_DATA, { const localStatsCardSVG = renderStatsCard(STATS_DATA);
include_all_commits: true,
});
// Get the Vercel preview stats card response. // Get the Vercel preview stats card response.
const serverStatsSvg = await axios.get( const serverStatsSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`, `${VERCEL_PREVIEW_URL}/api?username=${USER}&${CACHE_BURST_STRING}`,
); );
// Check if stats card from deployment matches the stats card from local. // Check if stats card from deployment matches the stats card from local.
+1 -3
View File
@@ -387,9 +387,7 @@ describe("Test renderStatsCard", () => {
document.querySelector( document.querySelector(
'g[transform="translate(0, 25)"]>.stagger>.stat.bold', 'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
).textContent, ).textContent,
).toMatchInlineSnapshot( ).toMatchInlineSnapshot(`"累计提交数(commit (2023):"`);
`"累计提交数(commit (${new Date().getFullYear()}):"`,
);
expect( expect(
document.querySelector( document.querySelector(
'g[transform="translate(0, 50)"]>.stagger>.stat.bold', 'g[transform="translate(0, 50)"]>.stagger>.stat.bold',
+256
View File
@@ -0,0 +1,256 @@
// @ts-check
import { expect, it, describe } from "@jest/globals";
import {
validateQueryStringParam,
validateQueryStringParams,
InvalidQueryStringParamsError,
} from "../src/common/validate";
import { themes } from "../themes/index.js";
/**
* @typedef {import("../src/common/validate").DataTypes} DataTypes The data types.
*/
/**
* @type {Map<string, DataTypes>} The query string params data type map.
*/
const QUERYSTRING_PARAMS_DATA_TYPE_MAP = new Map([
["username", "string"],
["hide", "enum-array"],
["hide_title", "boolean"],
["hide_border", "boolean"],
["card_width", "number"],
["hide_rank", "boolean"],
["show_icons", "boolean"],
["include_all_commits", "boolean"],
["line_height", "number"],
["title_color", "string"],
["ring_color", "string"],
["icon_color", "string"],
["text_color", "string"],
["text_bold", "boolean"],
["bg_color", "string"],
["theme", "enum"],
["cache_seconds", "number"],
["exclude_repo", "array"],
["custom_title", "string"],
["locale", "enum"],
["disable_animations", "boolean"],
["border_radius", "number"],
["border_color", "string"],
["number_format", "enum"],
["rank_icon", "enum"],
["show", "enum-array"],
]);
describe("test query string param validation", () => {
// Tests for `validateQueryStringParam` function.
it("should validate a string", () => {
expect(validateQueryStringParam("string", "username", "anuraghazra")).toBe(
true,
);
});
it("should validate a boolean", () => {
expect(validateQueryStringParam("boolean", "hide_title", "true")).toBe(
true,
);
expect(validateQueryStringParam("boolean", "hide_title", "false")).toBe(
true,
);
expect(validateQueryStringParam("boolean", "hide_title", "invalid")).toBe(
false,
);
});
it("should validate a number", () => {
expect(validateQueryStringParam("number", "card_width", "300")).toBe(true);
expect(validateQueryStringParam("number", "card_width", "300.000")).toBe(
true,
);
expect(validateQueryStringParam("number", "card_width", "invalid")).toBe(
false,
);
});
it("should validate an enum", () => {
expect(validateQueryStringParam("enum", "theme", "dark")).toBe(true);
expect(validateQueryStringParam("enum", "theme", "merko")).toBe(true);
expect(validateQueryStringParam("enum", "theme", "invalid")).toBe(false);
expect(validateQueryStringParam("enum", "locale", "en")).toBe(true);
expect(validateQueryStringParam("enum", "locale", "invalid")).toBe(false);
expect(validateQueryStringParam("enum", "number_format", "short")).toBe(
true,
);
expect(validateQueryStringParam("enum", "number_format", "invalid")).toBe(
false,
);
expect(validateQueryStringParam("enum", "rank_icon", "github")).toBe(true);
expect(validateQueryStringParam("enum", "rank_icon", "invalid")).toBe(
false,
);
});
it("should validate an enum-array", () => {
expect(
validateQueryStringParam("enum-array", "hide", "stars,commits,prs"),
).toBe(true);
expect(
validateQueryStringParam(
"enum-array",
"show",
"reviews,discussions_started",
),
).toBe(true);
expect(
validateQueryStringParam(
"enum-array",
"hide",
"stars,commits,prs,invalid",
),
).toBe(false);
expect(validateQueryStringParam("enum-array", "hide", "invalid")).toBe(
false,
);
});
it("should validate an array", () => {
expect(
validateQueryStringParam("array", "exclude_repo", "repo1,repo2"),
).toBe(true);
});
// Tests for `validateQueryStringParams` function.
it("should validate query string params", () => {
expect(
validateQueryStringParams(
{
username: "anuraghazra",
hide: "stars,commits,prs",
hide_title: "true",
hide_border: "true",
card_width: "300",
hide_rank: "true",
show_icons: "true",
include_all_commits: "true",
line_height: "25",
title_color: "fff",
ring_color: "fff",
icon_color: "fff",
text_color: "fff",
text_bold: "true",
bg_color: "fff",
theme: "dark",
cache_seconds: "300",
exclude_repo: "repo1,repo2",
custom_title: "My Custom Title",
locale: "en",
disable_animations: "true",
border_radius: "0",
number_format: "long",
border_color: "fff",
rank_icon: "github",
show: "reviews,discussions_started",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
// This function returns void on successful run and throws an error on invalid query string params.
).toBe(undefined);
});
it("should validate query string params with extra params", () => {
expect(
validateQueryStringParams(
{
username: "anuraghazra",
hide: "stars,commits,prs",
hide_title: "true",
hide_border: "true",
card_width: "300",
hide_rank: "true",
show_icons: "true",
include_all_commits: "true",
line_height: "25",
title_color: "fff",
ring_color: "fff",
icon_color: "fff",
text_color: "fff",
text_bold: "true",
bg_color: "fff",
theme: "dark",
cache_seconds: "300",
exclude_repo: "repo1,repo2",
custom_title: "My Custom Title",
locale: "en",
disable_animations: "true",
border_radius: "0",
number_format: "long",
border_color: "fff",
rank_icon: "github",
show: "reviews,discussions_started",
invalid: "invalid",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
// This function returns void on successful run and throws an error on invalid query string params.
).toBe(undefined);
});
it("should throw correct error on invalid boolean param", () => {
expect(() =>
validateQueryStringParams(
{
hide_title: "invalid",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
).toThrow(
new InvalidQueryStringParamsError(
"Invalid query string param `hide_title` value: invalid",
"Expected: true or false",
),
);
});
it("should throw correct error on invalid number param", () => {
expect(() =>
validateQueryStringParams(
{
card_width: "invalid",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
).toThrow(
new InvalidQueryStringParamsError(
"Invalid query string param `card_width` value: invalid",
"Expected: a number",
),
);
});
it("should throw correct error on invalid enum param", () => {
expect(() =>
validateQueryStringParams(
{
theme: "invalid",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
).toThrow(
new InvalidQueryStringParamsError(
"Invalid query string param `theme` value: invalid",
`Expected: ${Object.keys(themes).join(", ")}`,
),
);
});
it("should throw correct error on invalid enum-array param", () => {
expect(() =>
validateQueryStringParams(
{
hide: "commits,prs,invalid",
},
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
),
).toThrow(
new InvalidQueryStringParamsError(
"Invalid query string param `hide` value: commits,prs,invalid",
"Expected: stars, commits, prs, issues, contribs",
),
);
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
"functions": { "functions": {
"api/*.js": { "api/*.js": {
"memory": 128, "memory": 128,
"maxDuration": 30 "maxDuration": 10
} }
}, },
"redirects": [ "redirects": [