Compare commits

...
23 Commits
Author SHA1 Message Date
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dfb07a69b9 ci(deps): Bump actions/checkout from 6.0.2 to 6.0.3 (#270)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2
to 6.0.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/df4cb1c069e1874edd31b4311f1884172cec0e10"><code>df4cb1c</code></a>
Update changelog for v6.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/2446">#2446</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1cce3390c2bfda521930d01229c073c7ff920824"><code>1cce339</code></a>
Fix checkout init for SHA-256 repositories (<a
href="https://redirect.github.com/actions/checkout/issues/2439">#2439</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/900f2210b1d28bbbd0bd22d17926b9e224e8f231"><code>900f221</code></a>
fix: expand merge commit SHA regex and add SHA-256 test cases (<a
href="https://redirect.github.com/actions/checkout/issues/2414">#2414</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0c366fd6a839edf440554fa01a7085ccba70ac98"><code>0c366fd</code></a>
Update changelog (<a
href="https://redirect.github.com/actions/checkout/issues/2357">#2357</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:41:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 41b51dcfd9 build(deps-dev): Bump @types/node from 24.13.0 to 24.13.1 in the typescript group across 1 directory (#276)
Bumps the typescript group with 1 update in the / directory:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@types/node` from 24.13.0 to 24.13.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:39:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7687c47d85 build(deps-dev): Bump the eslint group across 1 directory with 2 updates (#284)
Bumps the eslint group with 2 updates in the / directory:
[eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).

Updates `eslint-plugin-jsdoc` from 63.0.1 to 63.0.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/gajus/eslint-plugin-jsdoc/releases">eslint-plugin-jsdoc's
releases</a>.</em></p>
<blockquote>
<h2>v63.0.2</h2>
<h2><a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">63.0.2</a>
(2026-06-06)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)
(<a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c">319e84b</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/50a7fbcf4abf7b27ca2774f65397f2f74395ea41"><code>50a7fbc</code></a>
chore: update semver and devDeps.</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/6041995ba873b5641e3b9bd1f95de004b4b9e7da"><code>6041995</code></a>
docs: fix in output</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c"><code>319e84b</code></a>
fix: allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)</li>
<li>See full diff in <a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `typescript-eslint` from 8.60.1 to 8.61.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.61.0</h2>
<h2>8.61.0 (2026-06-08)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>ast-spec:</strong> change type of
<code>UnaryExpression.prefix</code> to always <code>true</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12372">#12372</a>)</li>
<li><strong>ast-spec:</strong> tighten types of
<code>ArrowFunction</code>, <code>YieldExpression</code>,
<code>TSTypePredicate</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12373">#12373</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>rule-schema-to-typescript-types:</strong> respect ECMAScript
line terminators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12374">#12374</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>lumir</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.61.0 (2026-06-08)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/16a5b247affc32af21b695cf96dfd75d7ded50a3"><code>16a5b24</code></a>
chore(release): publish 8.61.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7917d54fdf build(deps-dev): Bump @types/react from 19.2.16 to 19.2.17 in the react group across 1 directory (#283)
Bumps the react group with 1 update in the / directory:
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).

Updates `@types/react` from 19.2.16 to 19.2.17
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/react&package-manager=npm_and_yarn&previous-version=19.2.16&new-version=19.2.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:03 +02:00
Martin 7771a34790 several dependency upgrades (#282) 2026-06-17 20:52:00 +02:00
Martin 1c266ad51e dependency upgrades (#265) 2026-06-07 18:24:46 +02:00
Martin 6258d90604 fix non-mocked frontend requests (#263) 2026-06-07 10:04:19 +02:00
Martin 6d857d0f34 upgrade dependencies (#262) 2026-06-06 12:23:41 +02:00
github-actions[bot]andmartin-mfg e15deeac76 Update languages JSON (#254)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-06-06 09:24:43 +02:00
Martin b5d50fc28f debug playwright (#261) 2026-06-06 09:23:54 +02:00
Marco Pasqualetti 1d394c7fad chore(actions): don’t repeat-recent-requests on forks (#259)
I received a notification about a failed workflow run on my fork of this
repository:

*
https://github.com/marcalexiei/github-stats-extended/actions/runs/26996444955

It looks like the `repeat-recent-requests` workflow is being triggered
on forks, even though it should only run on the canonical repositories.
This PR adds an `if` condition to prevent it from running on forks.

The condition matches the one already used in the `update-languages`
workflow:


https://github.com/stats-organization/github-stats-extended/blob/96d141e8c717f42a1a29e67f64984a19182dd575/.github/workflows/update-langs.yml#L31-L36

---

Additionally, this PR renames `update_tags.yml` to `update-tags.yml` for
consistency with the repository's workflow naming convention.
2026-06-05 19:32:58 +02:00
Martin 76e09ce3b0 improve 'Responsive Card Theme' docs (#260)
- reorder subsections
- shorten `<picture>` code
2026-06-05 18:17:02 +02:00
Martin 96d141e8c7 update documentation, step 2 (#243) 2026-05-21 23:28:09 +02:00
martin-mfg 6310073428 update documentation, step 2 2026-05-21 21:45:14 +02:00
Martin 1d8aaf0fc8 update documentation (#242) 2026-05-21 21:21:31 +02:00
martin-mfg 4a2785e286 update documentation 2026-05-21 21:19:35 +02:00
Martin 16ea9afebe set correct version for 'core' package (#218) 2026-05-08 12:54:54 +02:00
Martin c707e6b7b1 ignore git checks when publishing (#216) 2026-05-08 12:11:03 +02:00
Martin 9dc4910d4b finish setup for npm publishing (#212) 2026-05-08 11:51:02 +02:00
Martin 5d1978011e add npm publishing (#211) 2026-05-07 19:08:16 +02:00
Martin d84cc27b9c fix browser_rendering layout (#208) 2026-05-06 15:25:33 +02:00
Takuma IMAMURAandmartin-mfg e43e55c2b7 feat: replace render-time text wrapping with browser-native wrapping (#191)
This PR uses `foreignObject` with CSS line-clamp to let the browser
handle text wrapping natively instead of manually wrapping on the
server. This provides better font-aware wrapping while keeping
server-side line estimation for SVG height calculation.

Fixes https://github.com/anuraghazra/github-readme-stats/issues/4862.

### Example card

<img width="400" height="150" alt="hyperfinitism-sample-repo"
src="https://github.com/user-attachments/assets/c3a58531-c689-4798-af02-f55ef3a90fa1"
/>

---------

Signed-off-by: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com>
Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-05-06 11:00:11 +02:00
Martin f884df6312 add top-langs customization to fork.md (#205) 2026-05-03 19:27:15 +02:00
34 changed files with 1592 additions and 1130 deletions
+4 -4
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -106,7 +106,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
+22 -10
View File
@@ -1,18 +1,30 @@
name: Update version tags
name: Publish core package to npm
on:
push:
branches-ignore:
- "**"
tags:
- "v*.*.*"
workflow_dispatch:
release:
types: [released]
permissions: {}
jobs:
update-semver:
release:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
- 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
@@ -15,6 +15,10 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
+18
View File
@@ -0,0 +1,18 @@
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
+19 -8
View File
@@ -5,13 +5,14 @@
<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 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.
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.
# 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)
- [Self-Hosting](#self-hosting)
- [Run It Yourself](#run-it-yourself)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
@@ -27,6 +28,16 @@ This project is an [extended version](docs/fork.md) of [github-readme-stats](htt
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:
@@ -55,13 +66,13 @@ As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://
# 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 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.
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! ❤️
# Contributing
Contributions are welcome!
+3 -3
View File
@@ -18,12 +18,12 @@
"devDependencies": {
"axios-mock-adapter": "2.1.0",
"express": "5.2.1",
"jsdom": "29.0.2",
"jsdom": "catalog:default",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "^1.15.2",
"axios": "catalog:default",
"@stats-organization/github-readme-stats-core": "workspace:^",
"pg": "^8.20.0"
"pg": "^8.21.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="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
"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">
<title id="titleId"/>
<desc id="descId"/>
<style>
@@ -44,7 +44,10 @@ 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 }
@@ -90,20 +93,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:</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>
<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>
</text>
<g transform="translate(30, 117)">
<g transform="translate(30, 105)">
<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(56.96875, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(53.67999954223633, 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(111.28125, 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(107.59999923706056, 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>
@@ -127,7 +130,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="192" viewBox="0 0 400 192" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
"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">
<title id="titleId"/>
<desc id="descId"/>
<style>
@@ -141,7 +144,10 @@ 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 }
@@ -187,20 +193,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:</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>
<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>
</text>
<g transform="translate(30, 117)">
<g transform="translate(30, 105)">
<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(56.96875, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(53.67999954223633, 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(111.28125, 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(107.59999923706056, 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,11 +100,14 @@ 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;
@@ -160,8 +163,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 text</tspan><tspan dy="1.2em" x="25">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</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
</text>
<g transform="translate(30, 75)">
@@ -170,11 +173,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(82.24375, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(80.53999967575072, 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(142.55625, 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(140.4199994087219, 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>
@@ -217,11 +220,14 @@ 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;
@@ -277,7 +283,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>
@@ -287,11 +293,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(82.24375, 0)"><g transform="translate(0, 0)">
</g><g transform="translate(80.53999967575072, 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(142.55625, 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(140.4199994087219, 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.11.2",
"@tailwindcss/vite": "^4.2.2",
"axios": "^1",
"@reduxjs/toolkit": "^2.12.0",
"@tailwindcss/vite": "^4.3.0",
"axios": "catalog:default",
"axios-cache-interceptor": "^1",
"daisyui": "^5.5.19",
"daisyui": "^5.5.23",
"@stats-organization/github-readme-stats-backend": "workspace:^",
"@stats-organization/github-readme-stats-core": "workspace:^",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-icons": "^5.6.0",
"react-loading-skeleton": "^3.3.1",
"react-redux": "^9.2.0",
"react-redux": "^9.3.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.14",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.1",
"@vitejs/plugin-react": "6.0.2",
"clsx": "2.1.1",
"tailwindcss": "4.2.2",
"tailwindcss": "4.3.0",
"vite": "catalog:default",
"vitest": "catalog:default"
},
+46 -52
View File
@@ -85,6 +85,50 @@ 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:
@@ -115,58 +159,6 @@ 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.
@@ -323,6 +315,7 @@ 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` |
@@ -368,6 +361,7 @@ 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
+18 -37
View File
@@ -1,12 +1,12 @@
# Deploy on your own
# Run It Yourself
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.
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.
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 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
## GitHub Action
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.
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.
Create `/.github/workflows/grs.yml` in your profile repo (`USERNAME/USERNAME`):
@@ -15,17 +15,21 @@ name: Update README cards
on:
schedule:
- cron: "0 3 * * *"
- cron: "0 0 * * *" # Runs once daily at midnight
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Generate stats card
uses: readme-tools/github-readme-stats-action@v1
uses: stats-organization/github-readme-stats-action@v2
with:
card: stats
options: username=${{ github.repository_owner }}&show_icons=true
@@ -34,14 +38,14 @@ jobs:
- name: Commit cards
run: |
git config user.name "github-actions"
git config user.email "github-actions@users.noreply.github.com"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add profile/*.svg
git commit -m "Update README cards" || exit 0
git push
```
Then embed from your profile README:
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):
```md
![Stats](./profile/stats.svg)
@@ -49,7 +53,7 @@ Then embed from your profile README:
See more options and examples in the [GitHub Readme Stats Action README](https://github.com/stats-organization/github-readme-stats-action#readme).
## Self-hosted (Vercel/Other)
## Self-hosted on Vercel
Running your own instance avoids public rate limits and gives you full control over caching, tokens, and private stats.
@@ -88,14 +92,11 @@ 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)
<details>
<summary><b>:hammer_and_wrench: Recommended: Step-by-step guide on setting up your own Vercel instance</b></summary>
<b>Recommended: Step-by-step guide on setting up your own Vercel instance</b>
1. Go to [vercel.com](https://vercel.com/).
2. Click on `Log in`.
@@ -122,27 +123,7 @@ 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: 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>
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.
### Available environment variables
+19 -9
View File
@@ -1,6 +1,6 @@
# Fork Information
This project is an actively maintained fork of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
This project is an actively maintained fork and extension of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
## Key Differences
@@ -28,6 +28,11 @@ 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.
@@ -67,16 +72,21 @@ 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 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.
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.
## Compatibility Notes
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.
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.
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.0.5",
"@eslint/compat": "2.1.0",
"@eslint/js": "10.0.1",
"@playwright/test": "1.59.1",
"@types/node": "24.12.2",
"@playwright/test": "1.60.0",
"@types/node": "24.13.1",
"@vitest/coverage-v8": "catalog:default",
"eslint": "10.2.1",
"eslint-import-resolver-typescript": "4.4.4",
"eslint": "10.4.1",
"eslint-import-resolver-typescript": "4.4.5",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-jsdoc": "62.9.0",
"eslint-plugin-jsdoc": "63.0.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"globals": "17.5.0",
"globals": "17.6.0",
"husky": "9.1.7",
"knip": "6.7.0",
"lint-staged": "16.4.0",
"prettier": "3.8.3",
"turbo": "2.9.6",
"knip": "6.16.1",
"lint-staged": "17.0.7",
"prettier": "3.8.4",
"turbo": "2.9.17",
"typescript": "6.0.3",
"typescript-eslint": "8.59.0",
"typescript-eslint": "8.61.0",
"vitest": "catalog:default"
},
"scripts": {
+1
View File
@@ -0,0 +1 @@
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).
+8 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@stats-organization/github-readme-stats-core",
"version": "2.0.0",
"version": "2.1.2",
"type": "module",
"homepage": "https://github-stats-extended.vercel.app/frontend",
"bugs": {
@@ -60,13 +60,15 @@
},
"files": [
"build",
"src"
"src",
"README.md"
],
"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",
@@ -81,14 +83,13 @@
"@testing-library/jest-dom": "6.9.1",
"@uppercod/css-to-object": "1.1.1",
"axios-mock-adapter": "2.1.0",
"js-yaml": "4.1.1",
"jsdom": "29.0.2",
"js-yaml": "4.2.0",
"jsdom": "catalog:default",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "^1.15.2",
"axios": "catalog:default",
"emoji-name-map": "^2.0.3",
"github-username-regex": "^1.0.0",
"word-wrap": "^1.2.5"
"github-username-regex": "^1.0.0"
}
}
+2
View File
@@ -23,6 +23,7 @@ export default async (
border_radius,
border_color,
show_owner,
browser_rendering,
hide_border,
},
pat = null,
@@ -59,6 +60,7 @@ 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,6 +23,7 @@ export default async (
card_width,
theme,
show_owner,
browser_rendering,
show,
show_icons,
number_format,
@@ -99,6 +100,7 @@ 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,
+56 -13
View File
@@ -8,16 +8,23 @@ 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.
@@ -43,6 +50,7 @@ const renderGistCard = (gistData, options = {}) => {
border_radius,
border_color,
show_owner = false,
browser_rendering = false,
hide_border = false,
} = options;
@@ -57,16 +65,50 @@ const renderGistCard = (gistData, options = {}) => {
theme,
});
const lineWidth = 59;
const linesLimit = 10;
const desc = parseEmojis(description || "No description provided");
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("");
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 lineHeight = descriptionLines > 3 ? 12 : 10;
const height =
@@ -124,16 +166,17 @@ const renderGistCard = (gistData, options = {}) => {
});
card.setCSS(`
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.description {
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor};
${browser_rendering ? wrappedTextStyles(textColor) : ""}
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.icon { fill: ${iconColor} }
`);
card.setHideBorder(hide_border);
return card.render(`
<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>
${descriptionSvg}
<g transform="translate(30, ${height - 75})">
${starAndForkCount}
+60 -25
View File
@@ -8,19 +8,23 @@ 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;
/**
@@ -81,6 +85,7 @@ const renderRepoCard = (repo, options = {}) => {
bg_color,
card_width_input,
show_owner = false,
browser_rendering = false,
show = [],
show_icons = true,
number_format = "short",
@@ -177,27 +182,56 @@ const renderRepoCard = (repo, options = {}) => {
const header = show_owner ? nameWithOwner : name;
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
const descriptionMaxLines = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: DESCRIPTION_MAX_LINES;
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 descriptionBoxWidth = card_width - 2 * X_OFFSET;
const descriptionSvg = multiLineDescription
.map(
(line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`,
)
.join("");
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 extraHeight = Object.keys(STATS).length
? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight
@@ -279,11 +313,14 @@ const renderRepoCard = (repo, options = {}) => {
card.setHideBorder(hide_border);
card.setHideTitle(false);
card.setCSS(`
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.description {
font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${colors.textColor};
${browser_rendering ? wrappedTextStyles(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;
@@ -316,9 +353,7 @@ const renderRepoCard = (repo, options = {}) => {
: ""
}
<text class="description" x="${X_OFFSET}" y="-5">
${descriptionSvg}
</text>
${descriptionSvg}
<g transform="translate(30, ${height - 75 - extraHeight})">
${starAndForkCount}
+2
View File
@@ -34,6 +34,7 @@ export type StatCardOptions = CommonOptions & {
export type RepoCardOptions = CommonOptions & {
show_owner: boolean;
browser_rendering: boolean;
description_lines_count: number;
card_width_input;
show: Array<string>;
@@ -73,4 +74,5 @@ export type WakaTimeOptions = CommonOptions & {
export type GistCardOptions = CommonOptions & {
show_owner: boolean;
browser_rendering: boolean;
};
+8 -19
View File
@@ -1,8 +1,7 @@
// @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.
@@ -57,26 +56,16 @@ const formatBytes = (bytes) => {
* Split text over multiple lines based on the card width.
*
* @param {string} text Text to split.
* @param {number} width Line width in number of characters.
* @param {number} width Available wrap width in px.
* @param {number} fontSize Font size in px.
* @param {number} maxLines Maximum number of lines.
* @returns {string[]} Array of 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
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
// Add "..." to the last line if the text exceeds maxLines
if (wrapped.length > maxLines) {
@@ -254,6 +254,7 @@
"Hy": "#7790B2",
"IDL": "#a3522f",
"IGOR Pro": "#0000cc",
"IL Assembly": "#512BD4",
"INI": "#d1dbe0",
"ISPC": "#2D68B1",
"Idris": "#b30000",
@@ -467,6 +468,7 @@
"Quake": "#882233",
"QuakeC": "#975777",
"QuickBASIC": "#008080",
"Quint": "#9d6ce5",
"R": "#198CE7",
"RAML": "#77d9fb",
"RAScript": "#2C97FA",
+227 -23
View File
@@ -86,6 +86,66 @@ 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.
*
@@ -187,7 +247,7 @@ const renderError = ({
};
/**
* Retrieve text length.
* Retrieve text length based on Segoe UI font.
*
* @see https://stackoverflow.com/a/48172630/10629172
* @param {string} str String to measure.
@@ -197,37 +257,177 @@ 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.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,
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,
];
const avg = 0.5279276315789471;
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]/;
return (
str
.split("")
.map((c) =>
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
)
.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;
}
})
.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,
@@ -235,4 +435,8 @@ 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, 90, 1)[0],
wrapTextMultiline(res.data.errors[0].message, 525, 12)[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, 90, 1)[0],
wrapTextMultiline(res.data.errors[0].message, 525, 12)[0],
res.statusText,
);
}
+16 -6
View File
@@ -72,7 +72,12 @@ describe("Test fmt.js", () => {
it("wrapTextMultiline: should not wrap small texts", () => {
{
let multiLineText = wrapTextMultiline("Small text should not wrap");
let multiLineText = wrapTextMultiline(
"Small text should not wrap",
130,
11,
3,
);
expect(multiLineText).toEqual(["Small text should not wrap"]);
}
});
@@ -80,26 +85,31 @@ describe("Test fmt.js", () => {
it("wrapTextMultiline: should wrap large texts", () => {
let multiLineText = wrapTextMultiline(
"Hello world long long long text",
20,
130,
11,
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",
10,
53,
11,
2,
);
expect(multiLineText).toEqual(["Hello", "world long..."]);
});
it("wrapTextMultiline: should wrap chinese by punctuation", () => {
it("wrapTextMultiline: should handle chinese characters", () => {
let multiLineText = wrapTextMultiline(
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
130,
11,
3,
);
expect(multiLineText.length).toEqual(3);
expect(multiLineText[0].length).toEqual(18 * 8); // &#xxxxx; x 8
expect(multiLineText[0].length).toEqual(11 * 8); // &#xxxxx; x 8
});
});
+73 -3
View File
@@ -3,10 +3,80 @@
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
import { renderError } from "../src/common/render.js";
import {
countWrappedLines,
renderError,
splitWrappedText,
} from "../src/common/render.js";
describe("Test render.js", () => {
it("should test renderError", () => {
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", () => {
document.body.innerHTML = renderError({ message: "Something went wrong" });
expect(
queryByTestId(document.body, "message")?.children[0],
+22 -2
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 os too long", () => {
it("should trim description if description is too long", () => {
document.body.innerHTML = renderGistCard({
...data,
description:
@@ -66,7 +66,27 @@ describe("test renderGistCard", () => {
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all");
).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);
});
it("should not trim description if it is short", () => {
+33 -2
View File
@@ -29,7 +29,8 @@ describe("Test renderRepoCard", () => {
expect(header).toHaveTextContent("convoychat");
expect(header).not.toHaveTextContent("anuraghazra");
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Help us take over the world! React + TS + GraphQL Chat App",
// no space between "Chat" and "App" because there's a line break there
"Help us take over the world! React + TS + GraphQL ChatApp",
);
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
@@ -75,7 +76,7 @@ describe("Test renderRepoCard", () => {
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all");
).toBe("English-language pangram—a sentence that contains all of");
// Should not trim
document.body.innerHTML = renderRepoCard({
@@ -88,6 +89,36 @@ 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("305.81250000000006");
).toBe("299.9666657447815");
});
it("should auto resize if hide_rank is true & custom_title is set", () => {
+860 -854
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -3,9 +3,11 @@ packages:
- packages/*
catalog:
"@vitest/coverage-v8": 4.1.4
vite: 8.0.10
vitest: 4.1.4
"@vitest/coverage-v8": 4.1.8
axios: ^1.17.0
jsdom: 29.1.1
vite: 8.0.16
vitest: 4.1.8
minimumReleaseAge: 10080