release v2.0.0 (#209)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -323,6 +323,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 +369,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
|
||||
|
||||
|
||||
+9
-8
@@ -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
|
||||
|
||||
@@ -25,6 +25,11 @@ The resolution of this most requested feature in github-readme-stats was [origin
|
||||
|
||||
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||
@@ -67,16 +72,12 @@ 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 I decided to implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork I have been in contact with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and we decided to join forces. github-stats-extended is now in the process of becoming the successor of github-readme-stats.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
If there should be any changes in github-readme-stats in the future, they will be synced into this fork.
|
||||
|
||||
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
||||
|
||||
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.15.2",
|
||||
"emoji-name-map": "^2.0.3",
|
||||
"github-username-regex": "^1.0.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
"github-username-regex": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Vendored
+2
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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+3000–U+9FFF (CJK Symbols/Punctuation, Hiragana,
|
||||
// Katakana, CJK Unified Ideographs incl. Extension A) plus U+FF00–U+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+AC00–U+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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Generated
-3
@@ -193,9 +193,6 @@ importers:
|
||||
github-username-regex:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
word-wrap:
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
devDependencies:
|
||||
'@testing-library/dom':
|
||||
specifier: 10.4.1
|
||||
|
||||
Reference in New Issue
Block a user