Compare commits

..
14 Commits
Author SHA1 Message Date
Martin 9236acd99d release 1.2.0 (#204) 2026-05-03 18:28:19 +02:00
martin-mfg 12b4e979a8 Merge branch 'master' into release-1.2.0 2026-05-03 14:26:37 +02:00
Martin 5c4ea2a797 merge master: documentation updates (#127) 2026-03-07 20:52:27 +01:00
Martin 7d666e2510 merge master: fix dark mode colors in delete modal (#125) 2026-03-07 16:18:04 +01:00
Martin 96a18562f0 update release (#122)
Merging the latest `master` into `release` in order to create a new
release.
2026-03-07 14:36:04 +01:00
martin-mfg b64d601d47 Merge remote-tracking branch 'origin/master' into update-release 2026-03-07 14:34:01 +01:00
martin-mfg 26aec2a4af Merge remote-tracking branch 'origin/master' into release 2026-03-07 12:55:13 +01:00
martin-mfg ad23ab39ed Merge branch 'master' into release 2026-03-07 12:23:50 +01:00
martin-mfg d982bef3c5 Revert "fix problem on simultaneous login"
This reverts commit 23ba727b9a.
2026-01-08 18:57:49 +01:00
martin-mfg 2463f0900d Merge branch 'master' into release 2026-01-08 18:51:11 +01:00
martin-mfg 23ba727b9a fix problem on simultaneous login 2026-01-07 11:55:03 +01:00
martin-mfg 6c625fb76b Merge branch 'master' into release 2026-01-07 11:44:26 +01:00
martin-mfg 0aafbbbdb5 back to release rewrite 2026-01-06 20:21:14 +01:00
martin-mfg 8e6e0fac47 back to relase env vars 2026-01-06 20:00:49 +01:00
35 changed files with 1125 additions and 1587 deletions
+4 -4
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -106,7 +106,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
+10 -22
View File
@@ -1,30 +1,18 @@
name: Publish core package to npm
name: Update version tags
on:
workflow_dispatch:
release:
types: [released]
push:
branches-ignore:
- "**"
tags:
- "v*.*.*"
permissions: {}
jobs:
release:
update-semver:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Verify core package
run: |
pnpm --filter @stats-organization/github-readme-stats-core run typecheck
pnpm test
pnpm build:packages
pnpm --filter @stats-organization/github-readme-stats-core pack --dry-run
- name: Publish core package to npm
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public --no-git-checks
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
@@ -15,10 +15,6 @@ on:
jobs:
triggerRepeatRecent:
if: |
github.repository == 'anuraghazra/github-readme-stats' ||
github.repository == 'stats-organization/github-readme-stats' ||
github.repository == 'stats-organization/github-stats-extended'
name: Trigger server to update cached data.
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
-18
View File
@@ -1,18 +0,0 @@
name: Update version tags
on:
push:
branches-ignore:
- "**"
tags:
- "v*.*.*"
permissions: {}
jobs:
update-semver:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
+8 -19
View File
@@ -5,14 +5,13 @@
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
</div>
This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
This project is an [extended version](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
# Table of Contents
- [Quick Start](#quick-start)
- [Migration from github-readme-stats](#migration-from-github-readme-stats)
- [Card Types](#card-types)
- [Advanced Customization](#advanced-customization)
- [Run It Yourself](#run-it-yourself)
- [Self-Hosting](#self-hosting)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
@@ -28,16 +27,6 @@ This project is the [extended, actively maintained successor](docs/fork.md) of [
As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!
# Migration from github-readme-stats
To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`:
```diff
- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical
+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical
```
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For more details see [Compatibility Notes](docs/fork.md#compatibility-notes).
# Card Types
- Show your GitHub statistics:
@@ -66,13 +55,13 @@ GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For
# Advanced Customization
The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
# Run It Yourself
If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel.
See [Run It Yourself](docs/deploy.md) for detailed instructions.
# Acknowledgements
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of their functionality I added several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend I added to the project is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
# Self-Hosting
Since the GitHub API only allows a limited number of requests per hour, the public instance of GitHub-Stats-Extended at https://github-stats-extended.vercel.app/api could possibly hit the rate limiter. If you host your own instance you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
See [Deploy on your own](docs/deploy.md) for various deployment options.
# Contributing
Contributions are welcome!
+3 -3
View File
@@ -18,12 +18,12 @@
"devDependencies": {
"axios-mock-adapter": "2.1.0",
"express": "5.2.1",
"jsdom": "catalog:default",
"jsdom": "29.0.2",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "catalog:default",
"axios": "^1.15.2",
"@stats-organization/github-readme-stats-core": "workspace:^",
"pg": "^8.21.0"
"pg": "^8.20.0"
}
}
@@ -30,7 +30,7 @@ exports[`Test /api/gist contract > should match the private missing-id response
exports[`Test /api/gist contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
"content": "<svg width="400" height="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
@@ -44,10 +44,7 @@ exports[`Test /api/gist contract > should match the public happy-path response s
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.icon { fill: #586069 }
@@ -93,20 +90,20 @@ exports[`Test /api/gist contract > should match the public happy-path response s
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish:</tspan><tspan dy="1.2em" x="25">name, continent, capital, dial code, country codes, TLD,</tspan><tspan dy="1.2em" x="25">and area in sq km. Lista de países y territorios en</tspan><tspan dy="1.2em" x="25">Inglés y Español: nombre, continente, capital,</tspan><tspan dy="1.2em" x="25">código de teléfono, códigos de país,</tspan><tspan dy="1.2em" x="25">dominio y área en km cuadrados. Updated 2023</tspan>
</text>
<g transform="translate(30, 105)">
<g transform="translate(30, 117)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
<text data-testid="lang-name" class="gray" x="15">JSON</text>
</g>
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(56.96875, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(111.28125, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
@@ -130,7 +127,7 @@ exports[`Test /api/gist contract > should match the public happy-path response s
exports[`Test /api/gist contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
"content": "<svg width="400" height="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
@@ -144,10 +141,7 @@ exports[`Test /api/gist contract > should match the public many-params response
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
}
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.icon { fill: #ff00aa }
@@ -193,20 +187,20 @@ exports[`Test /api/gist contract > should match the public many-params response
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish:</tspan><tspan dy="1.2em" x="25">name, continent, capital, dial code, country codes, TLD,</tspan><tspan dy="1.2em" x="25">and area in sq km. Lista de países y territorios en</tspan><tspan dy="1.2em" x="25">Inglés y Español: nombre, continente, capital,</tspan><tspan dy="1.2em" x="25">código de teléfono, códigos de país,</tspan><tspan dy="1.2em" x="25">dominio y área en km cuadrados. Updated 2023</tspan>
</text>
<g transform="translate(30, 105)">
<g transform="translate(30, 117)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
<text data-testid="lang-name" class="gray" x="15">JSON</text>
</g>
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(56.96875, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(111.28125, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
@@ -100,14 +100,11 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.stagger {
opacity: 0;
@@ -163,8 +160,8 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough text</tspan><tspan dy="1.2em" x="25">to wrap across multiple lines in the repository card.</tspan>
</text>
<g transform="translate(30, 75)">
@@ -173,11 +170,11 @@ exports[`Test /api/pin contract > should match the public happy-path response sn
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(82.24375, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(142.55625, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
@@ -220,14 +217,11 @@ exports[`Test /api/pin contract > should match the public many-params response s
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
}
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
.stagger {
opacity: 0;
@@ -283,7 +277,7 @@ exports[`Test /api/pin contract > should match the public many-params response s
<text class="description" x="25" y="-5">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable React, TypeScript and GraphQL...</tspan>
</text>
@@ -293,11 +287,11 @@ exports[`Test /api/pin contract > should match the public many-params response s
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(82.24375, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(142.55625, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<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"/>
</svg>
+10 -10
View File
@@ -13,18 +13,18 @@
"typecheck": "tsc -p tsconfig.typecheck.json"
},
"dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"@tailwindcss/vite": "^4.3.0",
"axios": "catalog:default",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.2.2",
"axios": "^1",
"axios-cache-interceptor": "^1",
"daisyui": "^5.5.23",
"daisyui": "^5.5.19",
"@stats-organization/github-readme-stats-backend": "workspace:^",
"@stats-organization/github-readme-stats-core": "workspace:^",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"react-loading-skeleton": "^3.3.1",
"react-redux": "^9.3.0",
"react-redux": "^9.2.0",
"react-spinners": "^0.17.0",
"react-toastify": "^11.1.0",
"redux": "^5.0.1",
@@ -32,11 +32,11 @@
"uuid": "^14.0.0"
},
"devDependencies": {
"@types/react": "19.2.17",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.2",
"@vitejs/plugin-react": "6.0.1",
"clsx": "2.1.1",
"tailwindcss": "4.3.0",
"tailwindcss": "4.2.2",
"vite": "catalog:default",
"vitest": "catalog:default"
},
+1 -1
View File
@@ -1,6 +1,6 @@
export const USE_LOGGER = true as boolean;
export const CLIENT_ID = "Ov23lilAc5biyyRY0K1u";
export const CLIENT_ID = "Ov23liZSweT9LJrck9i8";
export const HOST = window.location.host;
+52 -46
View File
@@ -85,50 +85,6 @@ You can look at a preview for [all available themes](../packages/core/src/themes
Since GitHub will re-upload the cards and serve them from their [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), we can not infer the browser/GitHub theme on the server side. There are, however, four methods you can use to create dynamics themes on the client side.
##### Use GitHub's new media feature (recommended)
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
```html
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
media="(prefers-color-scheme: dark)"
/>
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
</picture>
```
<details>
<summary>:eyes: Show example</summary>
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
media="(prefers-color-scheme: dark)"
/>
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" /> <!-- light mode -->
</picture>
</details>
##### Use GitHub's theme context tag
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
```md
[![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
[![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=default#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
```
<details>
<summary>:eyes: Show example</summary>
[![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra\&show_icons=true\&theme=dark#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
[![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra\&show_icons=true\&theme=default#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
</details>
##### Use the transparent theme
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
@@ -159,6 +115,58 @@ You can use the `bg_color` parameter to make any of [the available themes](../pa
</details>
##### Use GitHub's theme context tag
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
```md
[![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
[![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=default#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
```
<details>
<summary>:eyes: Show example</summary>
[![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra\&show_icons=true\&theme=dark#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only)
[![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra\&show_icons=true\&theme=default#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only)
</details>
##### Use GitHub's new media feature
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
```html
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
media="(prefers-color-scheme: dark)"
/>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
</picture>
```
<details>
<summary>:eyes: Show example</summary>
<picture>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
media="(prefers-color-scheme: dark)"
/>
<source
srcset="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/>
<img src="https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true" />
</picture>
</details>
### Customization
You can customize the appearance of all your cards however you wish with URL parameters.
@@ -315,7 +323,6 @@ You can customize the appearance and behavior of the pinned repository card usin
| Name | Description | Type | Default value |
| --- | --- | --- | --- |
| `show_owner` | Shows the repo's owner name. | boolean | `false` |
| `browser_rendering` | Compute text wrapping of repository description natively in the browser, instead of computing it server-side. | boolean | `false` |
| `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` |
| `card_width` | Sets the card's width manually. | number | `400px (approx.)` |
| `show_icons` | Shows icons near all stats enabled via `show`. | boolean | `true` |
@@ -361,7 +368,6 @@ You can customize the appearance and behavior of the gist card using the [common
| Name | Description | Type | Default value |
| --- | --- | --- | --- |
| `show_owner` | Shows the gist's owner name. | boolean | `false` |
| `browser_rendering` | Compute text wrapping of gist description natively in the browser, instead of computing it server-side. | boolean | `false` |
### Demo
+37 -18
View File
@@ -1,12 +1,12 @@
# Run It Yourself
# Deploy on your own
We cache generated cards for a few hours to avoid potential rate-limiting in the GitHub API or on Vercel. If you want to set your own cache duration or you want to include private contributions in your stats without granting our hosted version of GitHub-Stats-Extended access to your private contributions, you can run GitHub-Stats-Extended on your own.
Since the GitHub API only allows a limited number of requests per hour, my `https://github-stats-extended.vercel.app/api` could possibly hit the rate limiter. If you deploy it yourself via GitHub Actions or your own hosted instance, then you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting GitHub-Stats-Extended on Vercel takes more work and can serve fresher stats (with caching).
GitHub Actions is the simplest setup with static SVGs stored in your repo but less frequent updates, while self-hosting takes more work and can serve fresher stats (with caching).
## GitHub Action
## GitHub Actions
With [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) you can generate static cards in your GitHub Actions workflow, commit them to your profile repository, and embed them directly from there. This avoids any per-request API calls.
GitHub Actions generates static SVGs and avoids per-request API calls. By default it uses `GITHUB_TOKEN` (public stats only), for private stats, set a [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) as a secret and pass it to the action instead.
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
@@ -15,21 +15,17 @@ name: Update README cards
on:
schedule:
- cron: "0 0 * * *" # Runs once daily at midnight
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Generate stats card
uses: stats-organization/github-readme-stats-action@v2
uses: readme-tools/github-readme-stats-action@v1
with:
card: stats
options: username=${{ github.repository_owner }}&show_icons=true
@@ -38,14 +34,14 @@ jobs:
- name: Commit cards
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions"
git config user.email "github-actions@users.noreply.github.com"
git add profile/*.svg
git commit -m "Update README cards" || exit 0
git push
```
Then embed from your [profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme):
Then embed from your profile README:
```md
![Stats](./profile/stats.svg)
@@ -53,7 +49,7 @@ Then embed from your [profile README](https://docs.github.com/en/account-and-pro
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
## Self-hosted on Vercel
## Self-hosted (Vercel/Other)
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
@@ -92,11 +88,14 @@ Selecting the right scopes for your token is important in case you want to displ
### On Vercel
<b>:film\_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)</b>
Click on the deploy button to get started!
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/stats-organization/github-stats-extended)
<b>Recommended: Step-by-step guide on setting up your own Vercel instance</b>
<details>
<summary><b>:hammer_and_wrench: Recommended: Step-by-step guide on setting up your own Vercel instance</b></summary>
1. Go to [vercel.com](https://vercel.com/).
2. Click on `Log in`.
@@ -123,7 +122,27 @@ Click on the deploy button to get started!
12. Click deploy, and you're good to go. See your domains to use the API!
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
15. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed.
16. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
</details>
### On other platforms
> [!WARNING]
> This way of using GitHub-Stats-Extended is not officially supported and was added to cater to some particular use cases where Vercel could not be used (e.g. [#2341](https://github.com/anuraghazra/github-readme-stats/discussions/2341)). The support for this method, therefore, is limited.
<details>
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>
1. Fork or clone this repo as per your needs
2. Move `express` from the devDependencies to the dependencies section of `package.json`
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61>
3. Run `npm i` if needed (initial setup)
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11>
5. You're done 🎉
</details>
### Available environment variables
+9 -19
View File
@@ -1,6 +1,6 @@
# Fork Information
This project is an actively maintained fork and extension of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
This project is an actively maintained fork of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
## Key Differences
@@ -28,11 +28,6 @@ GitHub-Stats-Extended proactively precomputes and caches cards. This solves the
### Multi-page fetching for accurate star counts
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
### Customization of top languages card
GitHub-Stats-Extended allows you to display your top languages without any numbers via the `hide_values` parameter. And the `prog_bar_bg_color` parameter allows you to customize the background color of the progress bars, e.g. by setting it to white:
![anuraghazra's top languages without numbers](https://github-stats-extended.vercel.app/api/top-langs?username=anuraghazra&langs_count=4&hide_values=true&prog_bar_bg_color=fff)
### Private contributions support
GitHub-Stats-Extended can include private contributions in your stats cards. You no longer have to deploy your own instance for that. Just log into the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) via the "GitHub Private Access" button (or click "Upgrade to Private Access" if already logged in). This will allow GitHub-Stats-Extended to see your private contributions.
@@ -72,21 +67,16 @@ One of the valued maintainers [wrote](https://github.com/anuraghazra/github-read
>
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
So [@martin-mfg](https://github.com/martin-mfg) decided to fork the project, implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork @martin-mfg joined forces with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and GitHub-Stats-Extended is now becoming the successor of github-readme-stats.
So I decided to implement some of the highly requested features and make the enhanced project available to everyone.
## Upstream Sync Strategy
The maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) keep their project up to date with dependency upgrades and do occasionally merge some pull requests. I regularly merge the latest upstream changes into this fork to keep it in sync with the upstream repo.
I also try to structure my own changes in a way that minimizes merge conflicts with the upstream repo. So some of my changes might even be merged back into [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) if the upstream maintainers are interested.
## Compatibility Notes
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Additional functionality introduced in this fork has to be explicitly enabled via some parameter.
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
There is only one exception to this: GitHub-Stats-Extended improves line wrapping for multi-line gist and repository descriptions.
This should be an improvement for existing cards, but it still changes their appearance a bit.
Previously, line wrapping happened simply after 59 characters, with special handling for Chinese characters:
<img width="400" height="140" alt="character-based" src="https://github.com/user-attachments/assets/1cf7edba-7f6c-4a37-89d7-334cbe54f0f1" />
GitHub-Stats-Extended now takes the actual width of each character into account:
<img width="400" height="150" alt="new-server-side-calculation" src="https://github.com/user-attachments/assets/277b92df-b2a9-48be-bd2a-d28c1d6764c5" />
+12 -12
View File
@@ -4,25 +4,25 @@
"type": "module",
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"devDependencies": {
"@eslint/compat": "2.1.0",
"@eslint/compat": "2.0.5",
"@eslint/js": "10.0.1",
"@playwright/test": "1.60.0",
"@types/node": "24.13.1",
"@playwright/test": "1.59.1",
"@types/node": "24.12.2",
"@vitest/coverage-v8": "catalog:default",
"eslint": "10.4.1",
"eslint-import-resolver-typescript": "4.4.5",
"eslint": "10.2.1",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-jsdoc": "63.0.2",
"eslint-plugin-jsdoc": "62.9.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"globals": "17.6.0",
"globals": "17.5.0",
"husky": "9.1.7",
"knip": "6.16.1",
"lint-staged": "17.0.7",
"prettier": "3.8.4",
"turbo": "2.9.17",
"knip": "6.7.0",
"lint-staged": "16.4.0",
"prettier": "3.8.3",
"turbo": "2.9.6",
"typescript": "6.0.3",
"typescript-eslint": "8.61.0",
"typescript-eslint": "8.59.0",
"vitest": "catalog:default"
},
"scripts": {
-1
View File
@@ -1 +0,0 @@
Core library powering [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) and [GitHub Readme Stats Action](https://github.com/stats-organization/github-readme-stats-action).
+7 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@stats-organization/github-readme-stats-core",
"version": "2.1.2",
"version": "2.0.0",
"type": "module",
"homepage": "https://github-stats-extended.vercel.app/frontend",
"bugs": {
@@ -60,15 +60,13 @@
},
"files": [
"build",
"src",
"README.md"
"src"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"prepack": "pnpm run build",
"test": "vitest",
"test:update:snapshot": "vitest -u",
"test:e2e": "vitest --config vitest.config.e2e.ts",
@@ -83,13 +81,14 @@
"@testing-library/jest-dom": "6.9.1",
"@uppercod/css-to-object": "1.1.1",
"axios-mock-adapter": "2.1.0",
"js-yaml": "4.2.0",
"jsdom": "catalog:default",
"js-yaml": "4.1.1",
"jsdom": "29.0.2",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "catalog:default",
"axios": "^1.15.2",
"emoji-name-map": "^2.0.3",
"github-username-regex": "^1.0.0"
"github-username-regex": "^1.0.0",
"word-wrap": "^1.2.5"
}
}
-2
View File
@@ -23,7 +23,6 @@ export default async (
border_radius,
border_color,
show_owner,
browser_rendering,
hide_border,
},
pat = null,
@@ -60,7 +59,6 @@ export default async (
border_color,
locale: locale ? locale.toLowerCase() : null,
show_owner: parseBoolean(show_owner),
browser_rendering: parseBoolean(browser_rendering),
hide_border: parseBoolean(hide_border),
}),
};
-2
View File
@@ -23,7 +23,6 @@ export default async (
card_width,
theme,
show_owner,
browser_rendering,
show,
show_icons,
number_format,
@@ -100,7 +99,6 @@ export default async (
border_color,
card_width_input: parseInt(card_width, 10),
show_owner: parseBoolean(show_owner),
browser_rendering: parseBoolean(browser_rendering),
show: showStats,
show_icons: parseBoolean(show_icons),
number_format,
+13 -56
View File
@@ -8,23 +8,16 @@ import { icons } from "../common/icons.js";
import languageColors from "../common/languageColors.json" with { type: "json" };
import { parseEmojis } from "../common/ops.js";
import {
countWrappedLines,
createLanguageNode,
flexLayout,
iconWithLabel,
measureText,
wrappedTextNode,
wrappedTextStyles,
} from "../common/render.js";
const ICON_SIZE = 16;
const CARD_DEFAULT_WIDTH = 400;
const X_OFFSET = 25;
const HEADER_MAX_LENGTH = 35;
const DESCRIPTION_BOX_WIDTH = CARD_DEFAULT_WIDTH - 2 * X_OFFSET;
const DESCRIPTION_FONT_SIZE = 13;
const DESCRIPTION_LINE_HEIGHT_PX = 16;
const DESCRIPTION_MAX_LINES = 10;
/**
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
@@ -50,7 +43,6 @@ const renderGistCard = (gistData, options = {}) => {
border_radius,
border_color,
show_owner = false,
browser_rendering = false,
hide_border = false,
} = options;
@@ -65,50 +57,16 @@ const renderGistCard = (gistData, options = {}) => {
theme,
});
const lineWidth = 59;
const linesLimit = 10;
const desc = parseEmojis(description || "No description provided");
let descriptionLines, descriptionSvg;
if (browser_rendering) {
// The browser performs the actual text wrapping inside the foreignObject;
// we only estimate the line count server-side so the SVG can reserve enough
// height. The estimate uses measureText for font-aware widths instead of a
// fixed character count.
descriptionLines = countWrappedLines(
desc,
DESCRIPTION_FONT_SIZE,
DESCRIPTION_BOX_WIDTH,
DESCRIPTION_MAX_LINES,
);
descriptionSvg = wrappedTextNode({
text: desc,
x: X_OFFSET,
y: -3,
width: DESCRIPTION_BOX_WIDTH,
height: descriptionLines * DESCRIPTION_LINE_HEIGHT_PX + 10, // 10px extra for "descenders" like g, j, q, p, y
lineCount: descriptionLines,
className: "description",
testId: "description-text",
});
} else {
const linesLimit = 10;
const multiLineDescription = wrapTextMultiline(
desc,
DESCRIPTION_BOX_WIDTH,
DESCRIPTION_FONT_SIZE,
linesLimit,
);
descriptionLines = multiLineDescription.length;
descriptionSvg = multiLineDescription
.map(
(line) =>
`<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
)
.join("");
descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>`;
}
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
const descriptionLines = multiLineDescription.length;
const descriptionSvg = multiLineDescription
.map(
(line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
)
.join("");
const lineHeight = descriptionLines > 3 ? 12 : 10;
const height =
@@ -166,17 +124,16 @@ const renderGistCard = (gistData, options = {}) => {
});
card.setCSS(`
.description {
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor};
${browser_rendering ? wrappedTextStyles(textColor) : ""}
}
.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} }
`);
card.setHideBorder(hide_border);
return card.render(`
${descriptionSvg}
<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>
<g transform="translate(30, ${height - 75})">
${starAndForkCount}
+26 -61
View File
@@ -8,23 +8,19 @@ import { encodeHTML } from "../common/html.js";
import { icons } from "../common/icons.js";
import { buildSearchFilter, clampValue, parseEmojis } from "../common/ops.js";
import {
countWrappedLines,
createLanguageNode,
flexLayout,
iconWithLabel,
measureText,
wrappedTextNode,
wrappedTextStyles,
} from "../common/render.js";
import { repoCardLocales } from "../translations.js";
import { createTextNode } from "./stats.js";
const ICON_SIZE = 16;
const DESCRIPTION_LINE_WIDTH = 59;
const CARD_DEFAULT_WIDTH = 400;
const X_OFFSET = 25;
const DESCRIPTION_FONT_SIZE = 13;
const DESCRIPTION_LINE_HEIGHT_PX = 16;
const DESCRIPTION_MAX_LINES = 3;
/**
@@ -85,7 +81,6 @@ const renderRepoCard = (repo, options = {}) => {
bg_color,
card_width_input,
show_owner = false,
browser_rendering = false,
show = [],
show_icons = true,
number_format = "short",
@@ -182,56 +177,27 @@ const renderRepoCard = (repo, options = {}) => {
const header = show_owner ? nameWithOwner : name;
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
const desc = parseEmojis(description || "No description provided");
const descriptionBoxWidth = card_width - 2 * X_OFFSET;
const descriptionMaxLines = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: DESCRIPTION_MAX_LINES;
let descriptionLinesCount, descriptionSvg;
if (browser_rendering) {
// The browser performs the actual text wrapping inside the foreignObject;
// we only estimate the line count server-side so the SVG can reserve enough
// height. The estimate uses measureText for font-aware widths instead of a
// fixed character count.
descriptionLinesCount = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: countWrappedLines(
desc,
DESCRIPTION_FONT_SIZE,
descriptionBoxWidth,
DESCRIPTION_MAX_LINES,
);
descriptionSvg = wrappedTextNode({
text: desc,
x: X_OFFSET,
y: -3,
width: descriptionBoxWidth,
height: descriptionLinesCount * DESCRIPTION_LINE_HEIGHT_PX + 10, // 10px extra for "descenders" like g, j, q, p, y
lineCount: descriptionLinesCount,
className: "description",
testId: "description-text",
});
} else {
const descriptionMaxLines = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: DESCRIPTION_MAX_LINES;
const multiLineDescription = wrapTextMultiline(
desc,
descriptionBoxWidth,
DESCRIPTION_FONT_SIZE,
descriptionMaxLines,
);
descriptionLinesCount = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: multiLineDescription.length;
descriptionSvg = multiLineDescription
.map(
(line) =>
`<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
)
.join("");
descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>`;
}
const desc = parseEmojis(description || "No description provided");
const multiLineDescription = wrapTextMultiline(
desc,
Math.round(
(card_width - CARD_DEFAULT_WIDTH) / 5.93 + DESCRIPTION_LINE_WIDTH,
),
descriptionMaxLines,
);
const descriptionLinesCount = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: multiLineDescription.length;
const descriptionSvg = multiLineDescription
.map(
(line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
)
.join("");
const extraHeight = Object.keys(STATS).length
? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight
@@ -313,14 +279,11 @@ const renderRepoCard = (repo, options = {}) => {
card.setHideBorder(hide_border);
card.setHideTitle(false);
card.setCSS(`
.description {
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${colors.textColor};
${browser_rendering ? wrappedTextStyles(colors.textColor) : ""}
}
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.stagger {
opacity: 0;
@@ -353,7 +316,9 @@ const renderRepoCard = (repo, options = {}) => {
: ""
}
${descriptionSvg}
<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>
<g transform="translate(30, ${height - 75 - extraHeight})">
${starAndForkCount}
-2
View File
@@ -34,7 +34,6 @@ export type StatCardOptions = CommonOptions & {
export type RepoCardOptions = CommonOptions & {
show_owner: boolean;
browser_rendering: boolean;
description_lines_count: number;
card_width_input;
show: Array<string>;
@@ -74,5 +73,4 @@ export type WakaTimeOptions = CommonOptions & {
export type GistCardOptions = CommonOptions & {
show_owner: boolean;
browser_rendering: boolean;
};
+19 -8
View File
@@ -1,7 +1,8 @@
// @ts-check
import wrap from "word-wrap";
import { encodeHTML } from "./html.js";
import { splitWrappedText } from "./render.js";
/**
* Retrieves num with suffix k(thousands) precise to given decimal places.
@@ -56,16 +57,26 @@ const formatBytes = (bytes) => {
* Split text over multiple lines based on the card width.
*
* @param {string} text Text to split.
* @param {number} width Available wrap width in px.
* @param {number} fontSize Font size in px.
* @param {number} width Line width in number of characters.
* @param {number} maxLines Maximum number of lines.
* @returns {string[]} Array of lines.
*/
const wrapTextMultiline = (text, width, fontSize, maxLines = 3) => {
const wrapped = splitWrappedText(text, fontSize, width);
const lines = wrapped
.map((line) => encodeHTML(line.trim()))
.slice(0, maxLines); // Only consider maxLines lines
const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
const fullWidthComma = "";
const encoded = encodeHTML(text);
const isChinese = encoded.includes(fullWidthComma);
let wrapped;
if (isChinese) {
wrapped = encoded.split(fullWidthComma); // Chinese full punctuation
} else {
wrapped = wrap(encoded, {
width,
}).split("\n"); // Split wrapped lines to get an array of lines
}
const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
// Add "..." to the last line if the text exceeds maxLines
if (wrapped.length > maxLines) {
@@ -254,7 +254,6 @@
"Hy": "#7790B2",
"IDL": "#a3522f",
"IGOR Pro": "#0000cc",
"IL Assembly": "#512BD4",
"INI": "#d1dbe0",
"ISPC": "#2D68B1",
"Idris": "#b30000",
@@ -468,7 +467,6 @@
"Quake": "#882233",
"QuakeC": "#975777",
"QuickBASIC": "#008080",
"Quint": "#9d6ce5",
"R": "#198CE7",
"RAML": "#77d9fb",
"RAScript": "#2C97FA",
+23 -227
View File
@@ -86,66 +86,6 @@ const createProgressNode = ({
`;
};
/**
* Renders multi-line text via a `foreignObject` so the browser performs
* native, font-aware wrapping. Content overflowing `lineCount` lines is
* clipped (with an ellipsis on the last visible line) by CSS line-clamp.
*
* @param {object} props Function properties.
* @param {string} props.text Text to render (will be HTML-encoded).
* @param {number} props.x X position of the foreignObject.
* @param {number} props.y Y position of the foreignObject.
* @param {number} props.width Width of the wrap box.
* @param {number} props.height Height of the wrap box.
* @param {number} props.lineCount Maximum number of lines to display.
* @param {string} props.className CSS class applied to the inner element.
* @param {string=} props.testId Optional test id for the inner element.
* @returns {string} foreignObject SVG node.
*/
const wrappedTextNode = ({
text,
x,
y,
width,
height,
lineCount,
className,
testId,
}) => {
const testIdAttr = testId ? ` data-testid="${testId}"` : "";
return `
<foreignObject x="${x}" y="${y}" width="${width}" height="${height}">
<div xmlns="http://www.w3.org/1999/xhtml" class="${className}" style="--lines: ${lineCount};"${testIdAttr}>${encodeHTML(
text,
)}</div>
</foreignObject>
`;
};
/**
* CSS rules used to render multi-line text inside a `foreignObject`. Apply this
* to a CSS class (e.g. `.description`) shared with `wrappedTextNode` so the
* browser handles wrapping and the line count is taken from the `--lines`
* custom property set on the element.
*
* @param {string} color Text color (CSS `color` property).
* @returns {string} CSS rules block (without the surrounding selector).
*/
const wrappedTextStyles = (color) => `
color: ${color};
margin: 0;
line-height: 1.2;
overflow-wrap: anywhere;
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines);
line-clamp: var(--lines);
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 0.15em;
`;
/**
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
*
@@ -247,7 +187,7 @@ const renderError = ({
};
/**
* Retrieve text length based on Segoe UI font.
* Retrieve text length.
*
* @see https://stackoverflow.com/a/48172630/10629172
* @param {string} str String to measure.
@@ -257,177 +197,37 @@ const renderError = ({
const measureText = (str, fontSize = 10) => {
// prettier-ignore
const widths = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0.2733333110809326, 0.28499999046325686,
0.39166667461395266, 0.5900000095367431, 0.5383333206176758,
0.8183333396911621, 0.8, 0.22999999523162842, 0.30166666507720946,
0.30166666507720946, 0.41666665077209475, 0.6833333492279052,
0.21666667461395264, 0.4, 0.21666667461395264, 0.3900000095367432,
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
0.5383333206176758, 0.5383333206176758, 0.5383333206176758,
0.5383333206176758, 0.21666667461395264, 0.21666667461395264,
0.6833333492279052, 0.6833333492279052, 0.6833333492279052,
0.4483333110809326, 0.9550000190734863, 0.6449999809265137,
0.5733333110809327, 0.6183333396911621, 0.7016666889190674,
0.5066666603088379, 0.48833332061767576, 0.6866666793823242,
0.7099999904632568, 0.26666667461395266, 0.35666666030883787,
0.5800000190734863, 0.4699999809265137, 0.8983333587646485,
0.7483333110809326, 0.753333330154419, 0.5599999904632569,
0.753333330154419, 0.5983333110809326, 0.5316666603088379,
0.5233333110809326, 0.6866666793823242, 0.621666669845581,
0.9333333015441895, 0.5900000095367431, 0.553333330154419,
0.5699999809265137, 0.30166666507720946, 0.37833333015441895,
0.30166666507720946, 0.6833333492279052, 0.41500000953674315,
0.26833333969116213, 0.5083333492279053, 0.5883333206176757,
0.4616666793823242, 0.5883333206176757, 0.5233333110809326,
0.3133333444595337, 0.5883333206176757, 0.5666666507720948,
0.24166667461395264, 0.24166667461395264, 0.49666666984558105,
0.24166667461395264, 0.8616666793823242, 0.5666666507720948,
0.5866666793823242, 0.5883333206176757, 0.5883333206176757,
0.3483333349227905, 0.425, 0.33833334445953367, 0.5666666507720948,
0.4783333301544189, 0.7233333110809326, 0.45833334922790525,
0.4833333492279053, 0.45166668891906736, 0.30166666507720946,
0.24000000953674316, 0.30166666507720946, 0.6833333492279052,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0.2796875, 0.2765625,
0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
];
const avg = 0.5131403493881227;
// CJK character range: U+3000U+9FFF (CJK Symbols/Punctuation, Hiragana,
// Katakana, CJK Unified Ideographs incl. Extension A) plus U+FF00U+FFEF
// (Halfwidth/Fullwidth Forms — fullwidth ASCII, fullwidth punctuation).
const cjkRange = /[\u3000-\u9FFF\uFF00-\uFFEF]/;
const avg = 0.5279276315789471;
return (
str
.split("")
.map((c) => {
if (cjkRange.test(c) || c === "\u3000") {
// CJK glyphs and U+3000 IDEOGRAPHIC SPACE are full-width by default;
return 1;
}
if (c.charCodeAt(0) < widths.length) {
return widths[c.charCodeAt(0)];
} else {
return avg;
}
})
.map((c) =>
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
)
.reduce((cur, acc) => acc + cur) * fontSize
);
};
/**
* Split text into the lines it would wrap to when laid out greedily at the
* given font size inside a box of width `maxWidth`. Uses `measureText` so the
* estimate reflects actual font metrics rather than a fixed character count.
* The browser still does the real wrap inside the foreignObject; this is only
* used to size the SVG.
*
* @param {string} text Text to split.
* @param {number} fontSize Font size in px (matches `measureText`).
* @param {number} maxWidth Available wrap width in px.
* @returns {string[]} Estimated wrapped lines.
*/
const splitWrappedText = (text, fontSize, maxWidth) => {
if (!text) {
return [];
}
// Tokenize the text into atoms representing line-break opportunities:
// - ASCII whitespace runs (collapsed/dropped at line edges per CSS rules);
// - non-ASCII whitespaces
// - a single CJK codepoint (browsers break between any two CJK chars,
// punctuation or not, so each one is its own atom and ~1 em wide);
// - a run of non-whitespace non-CJK characters (a "word" that can only
// break at its boundaries, like Latin script).
// Korean Hangul (U+AC00U+D7AF) is intentionally NOT in the CJK range
// because Korean wraps at word boundaries by default in HTML.
// ASCII whitespace is collapsed to a single space per CSS `white-space: normal;`
text = text.replace(/[\t\n\r ]+/g, " ");
const tokens = text.match(
/\s|[\u3000-\u9FFF\uFF00-\uFFEF]|[^\s\u3000-\u9FFF\uFF00-\uFFEF]+/g,
);
if (!tokens) {
return [];
}
const takeFittingSegment = (token, availableWidth) => {
const characters = token.split("");
let segment = "";
let width = 0;
for (const character of characters) {
const characterWidth = measureText(character, fontSize);
if (segment && width + characterWidth > availableWidth) {
break;
}
segment += character;
width += characterWidth;
}
return {
segment,
width,
};
};
const lines = [""];
let currentWidth = 0;
for (const token of tokens) {
if (token === " ") {
// Whitespace at the start of a line is dropped by browsers.
if (currentWidth === 0) {
continue;
}
lines[lines.length - 1] += token;
currentWidth += measureText(token, fontSize);
continue;
}
let remaining = token;
while (remaining) {
const w = measureText(remaining, fontSize);
if (currentWidth + w <= maxWidth) {
lines[lines.length - 1] += remaining;
currentWidth += w;
break;
}
if (currentWidth > 0) {
lines.push("");
currentWidth = 0;
continue;
}
// An atom wider than the box wraps mid-glyph (overflow-wrap: anywhere).
const { segment, width } = takeFittingSegment(remaining, maxWidth);
lines[lines.length - 1] += segment;
currentWidth = width;
remaining = remaining.slice(segment.length);
}
}
return lines.map((line) => line.replace(/[\t\n\r ]+$/, ""));
};
/**
* Estimate how many lines a string will wrap to when laid out greedily at the
* given font size inside a box of width `maxWidth`, capped at `maxLines`.
*
* @param {string} text Text to estimate.
* @param {number} fontSize Font size in px (matches `measureText`).
* @param {number} maxWidth Available wrap width in px.
* @param {number} maxLines Cap on the returned line count.
* @returns {number} Estimated line count, at least 1, at most `maxLines`.
*/
const countWrappedLines = (text, fontSize, maxWidth, maxLines) => {
return Math.min(
Math.max(1, splitWrappedText(text, fontSize, maxWidth).length),
maxLines,
);
};
export {
renderError,
createLanguageNode,
@@ -435,8 +235,4 @@ export {
iconWithLabel,
flexLayout,
measureText,
splitWrappedText,
countWrappedLines,
wrappedTextNode,
wrappedTextStyles,
};
+1 -1
View File
@@ -388,7 +388,7 @@ const fetchStats = async (
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 525, 12)[0],
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
+1 -1
View File
@@ -94,7 +94,7 @@ const fetchTopLanguages = async (
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 525, 12)[0],
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
+6 -16
View File
@@ -72,12 +72,7 @@ describe("Test fmt.js", () => {
it("wrapTextMultiline: should not wrap small texts", () => {
{
let multiLineText = wrapTextMultiline(
"Small text should not wrap",
130,
11,
3,
);
let multiLineText = wrapTextMultiline("Small text should not wrap");
expect(multiLineText).toEqual(["Small text should not wrap"]);
}
});
@@ -85,31 +80,26 @@ describe("Test fmt.js", () => {
it("wrapTextMultiline: should wrap large texts", () => {
let multiLineText = wrapTextMultiline(
"Hello world long long long text",
130,
11,
20,
3,
);
expect(multiLineText).toEqual(["Hello world long long", "long text"]);
expect(multiLineText).toEqual(["Hello world long", "long long text"]);
});
it("wrapTextMultiline: should wrap large texts and limit max lines", () => {
let multiLineText = wrapTextMultiline(
"Hello world long long long text",
53,
11,
10,
2,
);
expect(multiLineText).toEqual(["Hello", "world long..."]);
});
it("wrapTextMultiline: should handle chinese characters", () => {
it("wrapTextMultiline: should wrap chinese by punctuation", () => {
let multiLineText = wrapTextMultiline(
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
130,
11,
3,
);
expect(multiLineText.length).toEqual(3);
expect(multiLineText[0].length).toEqual(11 * 8); // &#xxxxx; x 8
expect(multiLineText[0].length).toEqual(18 * 8); // &#xxxxx; x 8
});
});
+3 -73
View File
@@ -3,80 +3,10 @@
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
import {
countWrappedLines,
renderError,
splitWrappedText,
} from "../src/common/render.js";
import { renderError } from "../src/common/render.js";
describe("Test splitWrappedText", () => {
it("should return an empty array for empty text", () => {
expect(splitWrappedText("", 10, 200)).toEqual([]);
});
it("should split a two-word string across lines", () => {
expect(splitWrappedText("hello world", 10, 25)).toEqual(["hello", "world"]);
});
it("should split a word wider than maxWidth", () => {
expect(splitWrappedText("aaaa", 10, 15)).toEqual(["aa", "aa"]);
});
it("should handle mix of short and long words", () => {
expect(splitWrappedText("short looooong", 10, 40)).toEqual([
"short",
"looooon",
"g",
]);
});
it("should handle complex whitespace characters", () => {
expect(splitWrappedText("One two three", 10, 25)).toEqual([
"One",
" ",
"",
"two",
"three",
]);
});
it("trailing spaces should not cause line breaks", () => {
expect(splitWrappedText("hi hi ", 10, 12)).toEqual(["hi", "hi"]);
});
});
describe("Test countWrappedLines", () => {
it("should return 1 for empty text", () => {
expect(countWrappedLines("", 10, 200, 10)).toBe(1);
});
it("should return 1 when all text fits on a single line", () => {
expect(countWrappedLines("hi", 10, 200, 10)).toBe(1);
});
it("should return 2 when a two-word string wraps", () => {
expect(countWrappedLines("hello world", 10, 25, 10)).toBe(2);
});
it("should split a word wider than maxWidth (overflow-wrap: anywhere)", () => {
expect(countWrappedLines("aaaa", 10, 15, 10)).toBe(2);
});
it("should cap the result at maxLines", () => {
expect(countWrappedLines("word ".repeat(10), 10, 25, 3)).toBe(3);
});
it("should handle complex whitespace characters", () => {
expect(countWrappedLines("One two three", 10, 25, 10)).toBe(5);
});
it("trailing spaces should not cause line breaks", () => {
expect(countWrappedLines("hi hi ", 10, 12, 10)).toBe(2);
});
});
describe("Test renderError", () => {
it("should contain error messages", () => {
describe("Test render.js", () => {
it("should test renderError", () => {
document.body.innerHTML = renderError({ message: "Something went wrong" });
expect(
queryByTestId(document.body, "message")?.children[0],
+2 -22
View File
@@ -54,7 +54,7 @@ describe("test renderGistCard", () => {
expect(header).toHaveTextContent("some-really-long-repo-name-for-test...");
});
it("should trim description if description is too long", () => {
it("should trim description if description os too long", () => {
document.body.innerHTML = renderGistCard({
...data,
description:
@@ -66,27 +66,7 @@ describe("test renderGistCard", () => {
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all of");
});
it("should respect browser_rendering=true", () => {
document.body.innerHTML = renderGistCard(
{
...data,
description:
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
},
{ browser_rendering: true },
);
// The full description stays in the DOM; the CSS line-clamp on the
// foreignObject's inner div is what visually truncates the overflow.
const description = document.getElementsByClassName("description")[0];
expect(description).toHaveTextContent(
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
);
expect(
Number(description.style.getPropertyValue("--lines")),
).toBeGreaterThan(0);
).toBe("English-language pangram—a sentence that contains all");
});
it("should not trim description if it is short", () => {
+2 -33
View File
@@ -29,8 +29,7 @@ describe("Test renderRepoCard", () => {
expect(header).toHaveTextContent("convoychat");
expect(header).not.toHaveTextContent("anuraghazra");
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
// no space between "Chat" and "App" because there's a line break there
"Help us take over the world! React + TS + GraphQL ChatApp",
"Help us take over the world! React + TS + GraphQL Chat App",
);
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
@@ -76,7 +75,7 @@ describe("Test renderRepoCard", () => {
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all of");
).toBe("English-language pangram—a sentence that contains all");
// Should not trim
document.body.innerHTML = renderRepoCard({
@@ -89,36 +88,6 @@ describe("Test renderRepoCard", () => {
);
});
it("should respect browser_rendering=true", () => {
document.body.innerHTML = renderRepoCard(
{
...data_repo.repository,
description:
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
},
{ browser_rendering: true },
);
// Browser-side wrapping inside the foreignObject keeps the full text in
// the DOM; the CSS line-clamp truncates whatever exceeds the line budget
// at render time.
const description = document.getElementsByClassName("description")[0];
expect(description).toHaveTextContent(
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
);
expect(description.style.getPropertyValue("--lines")).toBe("3");
// Short descriptions should leave the full text visible without clamping.
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
description: "Small text should not trim",
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Small text should not trim",
);
});
it("should render emojis", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
+1 -1
View File
@@ -350,7 +350,7 @@ describe("Test renderStatsCard", () => {
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
).toBe("299.9666657447815");
).toBe("305.81250000000006");
});
it("should auto resize if hide_rank is true & custom_title is set", () => {
+847 -853
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -3,11 +3,9 @@ packages:
- packages/*
catalog:
"@vitest/coverage-v8": 4.1.8
axios: ^1.17.0
jsdom: 29.1.1
vite: 8.0.16
vitest: 4.1.8
"@vitest/coverage-v8": 4.1.4
vite: 8.0.10
vitest: 4.1.4
minimumReleaseAge: 10080