release v2.0.0 (#209)

This commit is contained in:
Martin
2026-05-06 18:49:05 +02:00
committed by GitHub
parent 9236acd99d
commit 6ebd9a082f
20 changed files with 551 additions and 132 deletions
@@ -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>
+2
View File
@@ -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
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
@@ -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:
![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)
### 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.
+1 -2
View File
@@ -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"
}
}
+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) {
+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", () => {
-3
View File
@@ -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