diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 275c506a..38cf8c82 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-22.04
+FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-24.04
ADD first-run-notice.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..f14aa549
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+indent_size = 2
+
+[*.{js,css}]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
index dfe07704..5a0d5e48 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
# Auto detect text files and perform LF normalization
-* text=auto
+* text=auto eol=lf
diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml
index a04126ee..a2e6e21c 100644
--- a/.github/workflows/prettier.yml
+++ b/.github/workflows/prettier.yml
@@ -27,8 +27,13 @@ jobs:
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v3
+ - name: Setup Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '22'
+
- name: Install prettier and plugin-php
- run: npm install --global prettier@2.8.1 @prettier/plugin-php@0.18.9
+ run: npm i
- name: Lint with Prettier
continue-on-error: true
diff --git a/.gitignore b/.gitignore
index 24ee490f..ea2ef0ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,9 +12,10 @@ package-lock.json
# Environment
.env
+.php-version
DOCKER_ENV
docker_tag
# IDE
.vscode/
-.idea/
\ No newline at end of file
+.idea/
diff --git a/.prettierignore b/.prettierignore
index 22d0d82f..b5449b69 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1 +1,3 @@
vendor
+**/*.min.js
+.prettierrc
\ No newline at end of file
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 00000000..3a7657e1
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,13 @@
+module.exports = {
+ printWidth: 120,
+ endOfLine: "auto",
+ plugins: ["@prettier/plugin-php"],
+ overrides: [
+ {
+ files: "*.php",
+ options: {
+ parser: "php",
+ },
+ },
+ ],
+};
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..911749da
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,64 @@
+# Use PHP 8.3 (8.4 not supported yet)
+FROM php:8.3-apache@sha256:6be4ef702b2dd05352f7e5fe14667696a4ad091c9d2ad9083becbee4300dc3b1
+
+# Install system dependencies and PHP extensions in one layer
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ git \
+ unzip \
+ libicu-dev \
+ inkscape \
+ fonts-dejavu-core \
+ curl \
+ && docker-php-ext-configure intl \
+ && docker-php-ext-install intl \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install Composer
+COPY --from=composer/composer:latest-bin@sha256:c9bda63056674836406cacfbbdd8ef770fb4692ac419c967034225213c64e11b /composer /usr/bin/composer
+
+# Set working directory
+WORKDIR /var/www/html
+
+# Copy composer files and install dependencies
+COPY composer.json composer.lock ./
+COPY src/ ./src/
+RUN composer install --no-dev --optimize-autoloader --no-scripts
+
+# Configure Apache to serve from src/ directory and pass environment variables
+RUN a2enmod rewrite headers && \
+ echo 'ServerTokens Prod\n\
+ServerSignature Off\n\
+PassEnv TOKEN\n\
+PassEnv WHITELIST\n\
+\n\
+ ServerAdmin webmaster@localhost\n\
+ DocumentRoot /var/www/html/src\n\
+ \n\
+ Options -Indexes\n\
+ AllowOverride None\n\
+ Require all granted\n\
+ Header always set Access-Control-Allow-Origin "*"\n\
+ Header always set Content-Type "image/svg+xml" "expr=%{REQUEST_URI} =~ m#\\.svg$#i"\n\
+ Header always set Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; img-src data:;" "expr=%{REQUEST_URI} =~ m#\\.svg$#i"\n\
+ Header always set Referrer-Policy "no-referrer-when-downgrade"\n\
+ Header always set X-Content-Type-Options "nosniff"\n\
+ \n\
+ ErrorLog ${APACHE_LOG_DIR}/error.log\n\
+ CustomLog ${APACHE_LOG_DIR}/access.log combined\n\
+' > /etc/apache2/sites-available/000-default.conf
+
+# Set secure permissions
+RUN chown -R www-data:www-data /var/www/html && \
+ find /var/www/html -type d -exec chmod 755 {} \; && \
+ find /var/www/html -type f -exec chmod 644 {} \;
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+ CMD curl -f http://localhost/demo/ || exit 1
+
+# Expose port
+EXPOSE 80
+
+# Start Apache
+CMD ["apache2-foreground"]
diff --git a/README.md b/README.md
index dd45b6a9..adb16991 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ The following are the locales that have labels translated in Streak Stats. The `
-
en - English
 | am - አማርኛ
 | ar - العربية
 | bho - भोजपुरी
 | bn - বাংলা
 |
ca - català
 | ceb - Cebuano
 | da - dansk
 | de - Deutsch
 | el - Ελληνικά
 |
es - español
 | fa - فارسی
 | fil - Filipino
 | fr - français
 | gu - ગુજરાતી
 |
he - עברית
 | hi - हिन्दी
 | hu - magyar
 | id - Indonesia
 | it - italiano
 |
ja - 日本語
 | jv - Jawa
 | kn - ಕನ್ನಡ
 | ko - 한국어
 | mr - मराठी
 |
ms - Melayu
 | ms_ID - Melayu (Indonesia)
 | my - မြန်မာ
 | ne - नेपाली
 | nl - Nederlands
 |
no - norsk
 | pl - polski
 | ps - پښتو
 | pt - português
 | pt_BR - português (Brasil)
 |
ru - русский
 | sa - संस्कृत भाषा
 | sd_PK - سنڌي (پاڪستان)
 | sr - српски
 | su - Basa Sunda
 |
sw - Kiswahili
 | th - ไทย
 | tr - Türkçe
 | uk - українська
 | ur_PK - اردو (پاکستان)
 |
vi - Tiếng Việt
 | yo - Èdè Yorùbá
 | zh_Hans - 中文(简体)
 | zh_Hant - 中文(繁體)
 | bg - български
 |
ht - créole haïtien
 | hy - հայերեն
 | rw - Kinyarwanda
 | sv - svenska
 | ta - தமிழ்
 |
+en - English
 | am - አማርኛ
 | ar - العربية
 | bho - भोजपुरी
 | bn - বাংলা
 |
ca - català
 | ceb - Cebuano
 | da - dansk
 | de - Deutsch
 | el - Ελληνικά
 |
es - español
 | fa - فارسی
 | fil - Filipino
 | fr - français
 | gu - ગુજરાતી
 |
he - עברית
 | hi - हिन्दी
 | hu - magyar
 | id - Indonesia
 | it - italiano
 |
ja - 日本語
 | jv - Jawa
 | kn - ಕನ್ನಡ
 | ko - 한국어
 | mal - മലയാളം
 |
mi - Māori
 | mr - मराठी
 | ms - Melayu
 | ms_ID - Melayu (Indonesia)
 | my - မြန်မာ
 |
ne - नेपाली
 | nl - Nederlands
 | no - norsk
 | pa - ਪੰਜਾਬੀ
 | pl - polski
 |
ps - پښتو
 | pt - português
 | pt_BR - português (Brasil)
 | ru - русский
 | sa - संस्कृत भाषा
 |
sd_PK - سنڌي (پاڪستان)
 | sr_Cyrl - српски (ћирилица)
 | sr_Latn - srpski (latinica)
 | su - Basa Sunda
 | sv - svenska
 |
sw - Kiswahili
 | ta - தமிழ்
 | th - ไทย
 | tr - Türkçe
 | uk - українська
 |
ur_PK - اردو (پاکستان)
 | vi - Tiếng Việt
 | yo - Èdè Yorùbá
 | zh_Hans - 中文(简体)
 | zh_Hant - 中文(繁體)
 |
bg - български
 | ht - créole haïtien
 | hy - հայերեն
 | rw - Kinyarwanda
 | |
@@ -147,7 +147,7 @@ The longest streak is the highest number of consecutive days on which you have m
The current streak is the number of consecutive days ending with the current day on which you have made at least one contribution. If you have made a contribution today, it will be counted towards the current streak, however, if you have not made a contribution today, the streak will only count days before today so that your streak will not be zero.
-> [!NOTE]
+> [!NOTE]
> You may need to wait up to 24 hours for new contributions to show up ([Learn how contributions are counted](https://docs.github.com/articles/why-are-my-contributions-not-showing-up-on-my-profile))
## 📤 Deploying it on your own
@@ -164,7 +164,7 @@ The Inkscape dependency is required for PNG rendering, as well as Segoe UI font
Vercel is the recommended option for hosting the files since it is **free** and easy to set up. Watch the video below or expand the instructions to learn how to deploy to Vercel.
-> [!NOTE]
+> [!NOTE]
> PNG mode is not supported since Inkscape will not be installed but the default SVG mode will work.
### 📺 [Click here for a video tutorial on how to self-host on Vercel](https://www.youtube.com/watch?v=maoXtlb8t44)
@@ -210,14 +210,15 @@ Vercel is the recommended option for hosting the files since it is **free** and
9. Scroll to the bottom of the page and click on **"Generate token"**
10. Visit the Vercel dashboard at and select your project. Then, click on **"Settings"** and choose **"Environment Variables"**.
11. Add a new environment variable with the key `TOKEN` and the value as the token you generated in step 9, then save your changes
-12. To apply the new environment variable, you need to redeploy the app. Run `vercel --prod` to deploy the app to production.
+12. (Optional) You can also set the `WHITELIST` environment variable to restrict which GitHub usernames can be accessed through the service. Provide the usernames as a comma-separated list, for example: `user1,user2,user3`. If the variable is not set, information can be requested for any GitHub user.
+13. To apply the new environment variable(s), you need to redeploy the app. Run `vercel --prod` to deploy the app to production.

-> ⚠️ **Note**
+> ⚠️ **Note**
> To set up automatic Vercel deployments from GitHub, make sure to turn **off** "Include source files outside of the Root Directory" in the General settings and use `vercel` as the production branch in the Git settings.
-> ⚠️ **Note**
+> ⚠️ **Note**
> If you receive an error related to libssl or Node 20.x, you can fix this by opening your Vercel project settings and changing the Node.js version to 18.x.
>
> 
@@ -230,9 +231,9 @@ Heroku is another great option for hosting the files. All features are supported
Instructions for deploying to Heroku (Paid)
-
+
### Step-by-step instructions for deploying to Heroku
-
+
1. Sign in to **Heroku** or create a new account at
2. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token (no scopes required)
3. Scroll to the bottom and click **"Generate token"**
@@ -244,8 +245,9 @@ Heroku is another great option for hosting the files. All features are supported

-6. Click **"Deploy App"** at the end of the form
-7. Once the app is deployed, you can use `.herokuapp.com` in place of `streak-stats.demolab.com`
+6. (Optional) You can also set the `WHITELIST` Config Var to restrict which GitHub usernames can be accessed through the service. Provide the usernames as a comma-separated list, for example: `user1,user2,user3`. If the variable is not set, information can be requested for any GitHub user.
+7. Click **"Deploy App"** at the end of the form
+8. Once the app is deployed, you can use `.herokuapp.com` in place of `streak-stats.demolab.com`
@@ -253,6 +255,49 @@ Heroku is another great option for hosting the files. All features are supported
You can transfer the files to any webserver using FTP or other means, then refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for installation steps.
+### 🐳 Docker
+
+Docker is a great option for self-hosting with full control over your environment. All features are supported including PNG rendering with Inkscape. Expand the instructions below to learn how to deploy with Docker.
+
+
+ Instructions for deploying with Docker
+
+### Step-by-step instructions for deploying with Docker
+
+1. Clone the repository:
+
+ ```bash
+ git clone https://github.com/DenverCoder1/github-readme-streak-stats.git
+ cd github-readme-streak-stats
+ ```
+
+2. Visit https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats to create a new Personal Access Token (no scopes required)
+
+3. Scroll to the bottom and click "Generate token"
+
+4. Build the Docker image:
+
+ ```bash
+ docker build -t streak-stats .
+ ```
+
+5. Run the container with your GitHub token:
+
+ ```bash
+ docker run -d -p 8080:80 -e TOKEN=your_github_token_here streak-stats
+ ```
+
+6. You can also optionally set the `WHITELIST` environment variable to restrict which GitHub usernames can be accessed through the service. If the `WHITELIST` variable is not set, information can be requested for any GitHub user.
+ Provide the usernames as a comma-separated list, for example:
+
+ ```bash
+ docker run -d -p 8080:80 -e TOKEN=your_github_token_here -e WHITELIST=user1,user2,user3 streak-stats
+ ```
+
+7. Visit http://localhost:8080 to access your self-hosted instance
+
+
+
[hspace]: https://user-images.githubusercontent.com/20955511/136058102-b79570bc-4912-4369-b664-064a0ada8588.png
[verceldeploy]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDenverCoder1%2Fgithub-readme-streak-stats%2Ftree%2Fvercel&env=TOKEN&envDescription=GitHub%20Personal%20Access%20Token%20(no%20scopes%20required)&envLink=https%3A%2F%2Fgithub.com%2Fsettings%2Ftokens%2Fnew%3Fdescription%3DGitHub%2520Readme%2520Streak%2520Stats&project-name=streak-stats&repository-name=github-readme-streak-stats
[herokudeploy]: https://heroku.com/deploy?template=https://github.com/DenverCoder1/github-readme-streak-stats/tree/main
diff --git a/composer.json b/composer.json
index 82a95b97..63e189c3 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
"php -S localhost:8000 -t src"
],
"test": "./vendor/bin/phpunit --testdox tests",
- "lint": "prettier --check *.md **/*.{php,md,js,css} !**/*.min.js --print-width 120",
- "lint-fix": "prettier --write *.md **/*.{php,md,js,css} !**/*.min.js --print-width 120"
+ "lint": "npx prettier --check *.md **/*.{php,md,js,css}",
+ "lint-fix": "npx prettier --write *.md **/*.{php,md,js,css}"
}
}
diff --git a/composer.lock b/composer.lock
index 5fd881a5..ed0d9740 100644
--- a/composer.lock
+++ b/composer.lock
@@ -70,16 +70,16 @@
},
{
"name": "phpoption/phpoption",
- "version": "1.9.3",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
@@ -129,7 +129,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -141,24 +141,24 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:41:07+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.30.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@@ -169,8 +169,8 @@
"type": "library",
"extra": {
"thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
@@ -204,7 +204,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -215,29 +215,34 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.30.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "ext-iconv": "*",
+ "php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@@ -248,8 +253,8 @@
"type": "library",
"extra": {
"thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
@@ -284,7 +289,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -295,35 +300,39 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-06-19T12:30:46+00:00"
+ "time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.30.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
@@ -364,7 +373,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -375,25 +384,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2025-01-02T08:10:11+00:00"
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.6.1",
+ "version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
- "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
@@ -452,7 +465,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
@@ -464,22 +477,22 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:52:34+00:00"
+ "time": "2025-04-30T23:37:27+00:00"
}
],
"packages-dev": [
{
"name": "myclabs/deep-copy",
- "version": "1.12.1",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
- "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -518,7 +531,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -526,20 +539,20 @@
"type": "tidelift"
}
],
- "time": "2024-11-08T17:47:46+00:00"
+ "time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.4.0",
+ "version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
"shasum": ""
},
"require": {
@@ -558,7 +571,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -582,9 +595,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
},
- "time": "2024-12-30T11:07:19+00:00"
+ "time": "2025-08-13T20:13:15+00:00"
},
{
"name": "phar-io/manifest",
@@ -706,23 +719,23 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "11.0.8",
+ "version": "11.0.11",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "418c59fd080954f8c4aa5631d9502ecda2387118"
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118",
- "reference": "418c59fd080954f8c4aa5631d9502ecda2387118",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^5.3.1",
+ "nikic/php-parser": "^5.4.0",
"php": ">=8.2",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1",
@@ -734,7 +747,7 @@
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
- "phpunit/phpunit": "^11.5.0"
+ "phpunit/phpunit": "^11.5.2"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -772,15 +785,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
}
],
- "time": "2024-12-11T12:34:27+00:00"
+ "time": "2025-08-27T14:37:49+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -1029,16 +1054,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.7",
+ "version": "11.5.35",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d"
+ "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e1cb706f019e2547039ca2c839898cd5f557ee5d",
- "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91",
+ "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"shasum": ""
},
"require": {
@@ -1048,24 +1073,24 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.12.1",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
- "phpunit/php-code-coverage": "^11.0.8",
+ "phpunit/php-code-coverage": "^11.0.11",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
- "sebastian/code-unit": "^3.0.2",
- "sebastian/comparator": "^6.3.0",
+ "sebastian/code-unit": "^3.0.3",
+ "sebastian/comparator": "^6.3.2",
"sebastian/diff": "^6.0.2",
- "sebastian/environment": "^7.2.0",
+ "sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.0",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
- "sebastian/type": "^5.1.0",
+ "sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5"
},
@@ -1110,7 +1135,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.7"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35"
},
"funding": [
{
@@ -1121,12 +1146,20 @@
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
- "time": "2025-02-06T16:10:05+00:00"
+ "time": "2025-08-28T05:13:54+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1187,16 +1220,16 @@
},
{
"name": "sebastian/code-unit",
- "version": "3.0.2",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit.git",
- "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca"
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca",
- "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64",
"shasum": ""
},
"require": {
@@ -1232,7 +1265,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit/issues",
"security": "https://github.com/sebastianbergmann/code-unit/security/policy",
- "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2"
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3"
},
"funding": [
{
@@ -1240,7 +1273,7 @@
"type": "github"
}
],
- "time": "2024-12-12T09:59:06+00:00"
+ "time": "2025-03-19T07:56:08+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@@ -1300,16 +1333,16 @@
},
{
"name": "sebastian/comparator",
- "version": "6.3.0",
+ "version": "6.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115"
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115",
- "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
"shasum": ""
},
"require": {
@@ -1328,7 +1361,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "6.2-dev"
+ "dev-main": "6.3-dev"
}
},
"autoload": {
@@ -1368,15 +1401,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2025-01-06T10:28:19+00:00"
+ "time": "2025-08-10T08:07:46+00:00"
},
{
"name": "sebastian/complexity",
@@ -1505,23 +1550,23 @@
},
{
"name": "sebastian/environment",
- "version": "7.2.0",
+ "version": "7.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5"
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
- "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"suggest": {
"ext-posix": "*"
@@ -1557,15 +1602,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
- "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0"
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
}
],
- "time": "2024-07-03T04:54:44+00:00"
+ "time": "2025-05-21T11:55:47+00:00"
},
{
"name": "sebastian/exporter",
@@ -1881,23 +1938,23 @@
},
{
"name": "sebastian/recursion-context",
- "version": "6.0.2",
+ "version": "6.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "694d156164372abbd149a4b85ccda2e4670c0e16"
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16",
- "reference": "694d156164372abbd149a4b85ccda2e4670c0e16",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
@@ -1933,28 +1990,40 @@
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2"
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2024-07-03T05:10:34+00:00"
+ "time": "2025-08-13T04:42:22+00:00"
},
{
"name": "sebastian/type",
- "version": "5.1.0",
+ "version": "5.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac",
- "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
"shasum": ""
},
"require": {
@@ -1990,15 +2059,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
- "source": "https://github.com/sebastianbergmann/type/tree/5.1.0"
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
}
],
- "time": "2024-09-17T13:12:04+00:00"
+ "time": "2025-08-09T06:55:48+00:00"
},
{
"name": "sebastian/version",
@@ -2167,5 +2248,5 @@
"ext-intl": "*"
},
"platform-dev": [],
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/docs/themes.md b/docs/themes.md
index ad476960..ced17713 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -167,6 +167,11 @@ Note: Theme names provided are case-insensitive and any use of underscores will
| `rust-ferris-dark` |  |
| `cyber-streakglow` |  |
| `vitesse` |  |
+| `nord-aurora` |  |
+| `dark-aura` |  |
+| `everforest-dark` |  |
+| `everforest-light` |  |
+| `oceanic-next` |  |
### Can't find the theme you like?
diff --git a/package.json b/package.json
index fb08e235..628593a8 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
"engines": {
- "node": "18.x"
+ "node": "22.x"
},
"devDependencies": {
- "@prettier/plugin-php": "^0.18.8",
- "prettier": "^2.6.2"
+ "@prettier/plugin-php": "^0.24.0",
+ "prettier": "^3.6.2"
}
}
diff --git a/scripts/translation-progress.php b/scripts/translation-progress.php
index 30289d27..7d9c58b7 100644
--- a/scripts/translation-progress.php
+++ b/scripts/translation-progress.php
@@ -123,6 +123,6 @@ function updateReadme(string $path, string $start, string $end, string $content)
__DIR__ . "/../README.md",
"",
"",
- $badges
+ $badges,
);
exit($update === false ? 1 : 0);
diff --git a/src/card.php b/src/card.php
index b1532e83..501f80cc 100644
--- a/src/card.php
+++ b/src/card.php
@@ -27,7 +27,7 @@ function formatDate(string $dateString, string|null $format, string $locale): st
$locale,
IntlDateFormatter::MEDIUM,
IntlDateFormatter::NONE,
- pattern: $pattern
+ pattern: $pattern,
);
$formatted = $dateFormatter->format($date);
}
@@ -44,7 +44,7 @@ function formatDate(string $dateString, string|null $format, string $locale): st
$locale,
IntlDateFormatter::MEDIUM,
IntlDateFormatter::NONE,
- pattern: $pattern
+ pattern: $pattern,
);
$formatted = $dateFormatter->format($date);
}
@@ -75,7 +75,7 @@ function translateDays(array $days, string $locale): array
$locale,
IntlDateFormatter::NONE,
IntlDateFormatter::NONE,
- pattern: $pattern
+ pattern: $pattern,
);
$translatedDays = [];
foreach ($days as $day) {
@@ -250,7 +250,7 @@ function splitLines(string $text, int $maxChars, int $line1Offset): string
return preg_replace(
"/^(.*)\n(.*)/",
"$1$2",
- $text
+ $text,
);
}
@@ -752,7 +752,7 @@ function ($matches) {
$opacity = $result["opacity"];
return "{$attribute}='{$color}' {$opacityAttribute}='{$opacity}'";
},
- $svg
+ $svg,
);
return $svg;
diff --git a/src/demo/css/style.css b/src/demo/css/style.css
index f57b550d..852763a6 100644
--- a/src/demo/css/style.css
+++ b/src/demo/css/style.css
@@ -44,7 +44,10 @@ html {
body {
background: var(--background);
- font-family: Segoe UI, Ubuntu, sans-serif;
+ font-family:
+ Segoe UI,
+ Ubuntu,
+ sans-serif;
padding-top: 10px;
color: var(--text);
}
@@ -105,7 +108,9 @@ h2 {
border-radius: 6px;
cursor: pointer;
font-family: inherit;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
+ box-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 1px 2px rgba(0, 0, 0, 0.24);
transition: 0.2s ease-in-out;
}
@@ -115,7 +120,9 @@ h2 {
.btn:hover {
background-color: var(--blue-dark);
- box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
+ box-shadow:
+ 0 3px 6px rgba(0, 0, 0, 0.16),
+ 0 3px 6px rgba(0, 0, 0, 0.23);
}
.btn:disabled {
diff --git a/src/demo/css/toggle-dark.css b/src/demo/css/toggle-dark.css
index 1fe526f3..227a54ed 100644
--- a/src/demo/css/toggle-dark.css
+++ b/src/demo/css/toggle-dark.css
@@ -11,12 +11,16 @@ a.darkmode {
justify-content: center;
border-radius: 50%;
border: 2px solid var(--border);
- box-shadow: 0 0 3px rgb(0 0 0 / 12%), 0 1px 2px rgb(0 0 0 / 24%);
+ box-shadow:
+ 0 0 3px rgb(0 0 0 / 12%),
+ 0 1px 2px rgb(0 0 0 / 24%);
transition: 0.2s ease-in box-shadow;
}
a.darkmode:hover {
- box-shadow: 0 0 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
+ box-shadow:
+ 0 0 6px rgb(0 0 0 / 16%),
+ 0 3px 6px rgb(0 0 0 / 23%);
}
@media only screen and (max-width: 600px) {
diff --git a/src/demo/index.php b/src/demo/index.php
index 62f02c4e..81e97011 100644
--- a/src/demo/index.php
+++ b/src/demo/index.php
@@ -22,7 +22,7 @@ function camelToSkewer(string $str): string
function ($matches) {
return "-" . strtolower($matches[0]);
},
- $str
+ $str,
);
}
?>
diff --git a/src/demo/js/accordion.js b/src/demo/js/accordion.js
index 78e4e997..48e4deab 100644
--- a/src/demo/js/accordion.js
+++ b/src/demo/js/accordion.js
@@ -55,7 +55,7 @@ class Accordion {
{
duration: 400,
easing: "ease-out",
- }
+ },
);
// When the animation is complete, call onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(false);
@@ -93,7 +93,7 @@ class Accordion {
{
duration: 400,
easing: "ease-out",
- }
+ },
);
// When the animation is complete, call onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(true);
diff --git a/src/demo/js/script.js b/src/demo/js/script.js
index 8239318a..465614a5 100644
--- a/src/demo/js/script.js
+++ b/src/demo/js/script.js
@@ -143,7 +143,7 @@ const preview = {
format: "hexa",
onChange: `preview.pickerChange(this, '${color1.id}')`,
onInput: `preview.pickerChange(this, '${color1.id}')`,
- })
+ }),
);
const color2 = document.createElement("input");
color2.className = "param jscolor";
@@ -154,7 +154,7 @@ const preview = {
format: "hexa",
onChange: `preview.pickerChange(this, '${color2.id}')`,
onInput: `preview.pickerChange(this, '${color2.id}')`,
- })
+ }),
);
rotate.name = color1.name = color2.name = propertyName;
color1.value = color1Value;
@@ -484,5 +484,5 @@ window.addEventListener(
// update previews
preview.update();
},
- false
+ false,
);
diff --git a/src/stats.php b/src/stats.php
index 6511758c..902eb453 100644
--- a/src/stats.php
+++ b/src/stats.php
@@ -2,6 +2,8 @@
declare(strict_types=1);
+require_once "whitelist.php";
+
/**
* Build a GraphQL query for a contribution graph
*
@@ -121,6 +123,10 @@ function executeContributionGraphRequests(string $user, array $years): array
*/
function getContributionGraphs(string $user, ?int $startingYear = null): array
{
+ if (!isWhitelisted($user)) {
+ throw new InvalidArgumentException("User not in whitelist.", 403);
+ }
+
// get the list of years the user has contributed and the current year's contribution graph
$currentYear = intval(date("Y"));
$responses = executeContributionGraphRequests($user, [$currentYear]);
@@ -210,7 +216,7 @@ function removeGitHubToken(string $token): void
if (empty($GLOBALS["ALL_TOKENS"])) {
throw new AssertionError(
"We are being rate-limited! Check git.io/streak-ratelimit for details.",
- 429
+ 429,
);
}
}
@@ -287,7 +293,7 @@ function normalizeDays(array $days): array
$dayOfWeek = substr(ucfirst(strtolower(trim($dayOfWeek))), 0, 3);
// return day if valid, otherwise return null
return in_array($dayOfWeek, ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]) ? $dayOfWeek : null;
- }, $days)
+ }, $days),
);
}
diff --git a/src/themes.php b/src/themes.php
index e6fc193d..0e1972e5 100644
--- a/src/themes.php
+++ b/src/themes.php
@@ -2069,4 +2069,69 @@
"dates" => "#BD976A",
"excludeDaysLabel" => "#758575DD",
],
+ "everforest-dark" => [
+ "background" => "#2D353B",
+ "border" => "#4F585E",
+ "stroke" => "#4F585E",
+ "ring" => "#A7C080",
+ "fire" => "#A7C080",
+ "currStreakNum" => "#D3C6AA",
+ "sideNums" => "#A7C080",
+ "currStreakLabel" => "#D3C6AA",
+ "sideLabels" => "#A7C080",
+ "dates" => "#9DA9A0",
+ "excludeDaysLabel" => "#9DA9A0",
+ ],
+ "nord-aurora" => [
+ "background" => "#4C566A",
+ "border" => "#8FBCBB",
+ "stroke" => "#D8DEE9",
+ "ring" => "#A3BE8C",
+ "fire" => "#BF616A",
+ "currStreakNum" => "#A3BE8C",
+ "sideNums" => "#B48EAD",
+ "currStreakLabel" => "#EBCB8B",
+ "sideLabels" => "#D08770",
+ "dates" => "#88C0D0",
+ "excludeDaysLabel" => "#81A1C1",
+ ],
+ "dark-aura" => [
+ "background" => "#760A11",
+ "border" => "#310C69C5",
+ "stroke" => "#FF1D5E",
+ "ring" => "#C1184E",
+ "fire" => "#EB4511",
+ "currStreakNum" => "#EB5454",
+ "sideNums" => "#EB5454",
+ "currStreakLabel" => "#FF8F62",
+ "sideLabels" => "#FF8F62",
+ "dates" => "#EB5454",
+ "excludeDaysLabel" => "#758575DD",
+ ],
+ "everforest-light" => [
+ "background" => "#F2F4EF",
+ "border" => "#C7CCC2",
+ "stroke" => "#C7CCC2",
+ "ring" => "#7F9C6F",
+ "fire" => "#7F9C6F",
+ "currStreakNum" => "#55674E",
+ "sideNums" => "#7F9C6F",
+ "currStreakLabel" => "#55674E",
+ "sideLabels" => "#7F9C6F",
+ "dates" => "#8B9286",
+ "excludeDaysLabel" => "#8B9286",
+ ],
+ "oceanic-next" => [
+ "background" => "#1B2B34",
+ "border" => "#343D46",
+ "stroke" => "#4F5B66",
+ "ring" => "#6699CC",
+ "fire" => "#EC5F67",
+ "currStreakNum" => "#99C794",
+ "sideNums" => "#6699CC",
+ "currStreakLabel" => "#FAC863",
+ "sideLabels" => "#5FB3B3",
+ "dates" => "#A7ADBA",
+ "excludeDaysLabel" => "#A7ADBA",
+ ],
];
diff --git a/src/translations.php b/src/translations.php
index 260f89d4..e66bb4f4 100644
--- a/src/translations.php
+++ b/src/translations.php
@@ -280,7 +280,26 @@
"Week Streak" => "주간 연속 기여 수",
"Longest Week Streak" => "최장 주간 연속 기여 수",
"Present" => "현재",
- "Excluding {days}" => "{days} 제외하고",
+ "Excluding {days}" => "{days}를 제외하고",
+ ],
+ "mal" => [
+ "Total Contributions" => "മൊത്തം സംഭാവനകൾ",
+ "Current Streak" => "നിലവിലെ സ്ട്രീക്ക്",
+ "Longest Streak" => "ഏറ്റവും ദൈർഘ്യമേറിയ സ്ട്രീക്ക്",
+ "Week Streak" => "പ്രതിവാര സ്ട്രീക്ക്",
+ "Longest Week Streak" => "ദൈർഘ്യമേറിയ ആഴ്ച സ്ട്രീക്ക്",
+ "Present" => "ഇപ്പം",
+ "Excluding {days}" => "{days} ഒഴികെ",
+ "comma_separator" => "、",
+ ],
+ "mi" => [
+ "Total Contributions" => "Tapeke Tākoha",
+ "Current Streak" => "Raupapa Nāianei",
+ "Longest Streak" => "Raupapa Roa Rawa",
+ "Week Streak" => "Raupapa Wiki",
+ "Longest Week Streak" => "Raupapa Wiki Roa Rawa",
+ "Present" => "O Nāianei",
+ "Excluding {days}" => "Haunga {ngā rā}",
],
"mr" => [
"Total Contributions" => "एकूण योगदान",
@@ -345,6 +364,15 @@
"Present" => "I dag",
"Excluding {days}" => "Ekskluderer {days}",
],
+ "pa" => [
+ "Total Contributions" => "ਕੁੱਲ ਯੋਗਦਾਨ",
+ "Current Streak" => "ਮੌਜੂਦਾ ਲਗਾਤਾਰ ਦਿਨ",
+ "Longest Streak" => "ਸਭ ਤੋਂ ਲੰਬੀ ਲਗਾਤਾਰ ਸਿਰੀਂ",
+ "Week Streak" => "ਹਫ਼ਤਾ ਲਗਾਤਾਰ ਸਿਰੀਂ",
+ "Longest Week Streak" => "ਸਭ ਤੋਂ ਲੰਬੀ ਹਫ਼ਤਾਵਾਰੀ ਸਿਰੀਂ",
+ "Present" => "ਮੌਜੂਦ",
+ "Excluding {days}" => "{days} ਨੂੰ ਛੱਡ ਕੇ",
+ ],
"pl" => [
"Total Contributions" => "Suma Kontrybucji",
"Current Streak" => "Aktualna Seria",
@@ -420,15 +448,25 @@
"Excluding {days}" => "نڪتل {days}",
"comma_separator" => "، ",
],
- "sr" => [
- "Total Contributions" => "Укупно додавања",
+ "sr" => "sr_Cyrl",
+ "sr_Cyrl" => [
+ "Total Contributions" => "Укупно доприноса",
"Current Streak" => "Тренутна серија",
"Longest Streak" => "Најдужа серија",
"Week Streak" => "Недељна серија",
- "Longest Week Streak" => "Најдужа недељена серија",
+ "Longest Week Streak" => "Најдужа недељна серија",
"Present" => "Данас",
"Excluding {days}" => "Искључујући {days}",
],
+ "sr_Latn" => [
+ "Total Contributions" => "Ukupno doprinosa",
+ "Current Streak" => "Trenutna serija",
+ "Longest Streak" => "Najduža serija",
+ "Week Streak" => "Nedeljna serija",
+ "Longest Week Streak" => "Najduža nedeljna serija",
+ "Present" => "Danas",
+ "Excluding {days}" => "Isključujući {days}",
+ ],
"su" => [
"Total Contributions" => "Total Kontribusi",
"Current Streak" => "Aksi Ayeuna",
@@ -445,6 +483,7 @@
"Week Streak" => "Antal veckor i rad",
"Longest Week Streak" => "Längst antal veckor i rad",
"Present" => "Just nu",
+ "Excluding {days}" => "Utom {dagar}",
],
"sw" => [
"Total Contributions" => "Jumla ya Michango",
@@ -462,6 +501,7 @@
"Week Streak" => "வார\nபங்களிப்புகள்",
"Longest Week Streak" => "நீண்ட வார\nபங்களிப்புகள்",
"Present" => "இன்றுவரை",
+ "Excluding {days}" => "{days} தவிர",
],
"th" => [
"Total Contributions" => "คอนทริบิ้วต์ทั้งหมด",
diff --git a/src/whitelist.php b/src/whitelist.php
new file mode 100644
index 00000000..21114834
--- /dev/null
+++ b/src/whitelist.php
@@ -0,0 +1,13 @@
+assertEquals(
array_diff_key($colors, $this->defaultTheme),
[],
- "The theme '$theme' contains invalid parameters."
+ "The theme '$theme' contains invalid parameters.",
);
# check that no parameters are missing and all values are valid
foreach (array_keys($this->defaultTheme) as $param) {
@@ -85,7 +85,7 @@ public function testThemesHaveValidParameters(): void
$this->assertMatchesRegularExpression(
$backgroundRegex,
$colors[$param],
- "The parameter '$param' of '$theme' is not a valid background value."
+ "The parameter '$param' of '$theme' is not a valid background value.",
);
continue;
}
@@ -93,13 +93,13 @@ public function testThemesHaveValidParameters(): void
$this->assertMatchesRegularExpression(
$hexRegex,
strtoupper($colors[$param]),
- "The parameter '$param' of '$theme' is not a valid hex color."
+ "The parameter '$param' of '$theme' is not a valid hex color.",
);
// check that the key is a valid hex color in uppercase
$this->assertMatchesRegularExpression(
$hexRegex,
$colors[$param],
- "The parameter '$param' of '$theme' should not contain lowercase letters."
+ "The parameter '$param' of '$theme' should not contain lowercase letters.",
);
}
}
@@ -251,7 +251,7 @@ public function testAllThemeNamesNormalized(): void
$this->assertEquals(
$theme,
$normalized,
- "Theme name '$theme' is not normalized. It should contain only lowercase letters, numbers, and dashes. Consider renaming it to '$normalized'."
+ "Theme name '$theme' is not normalized. It should contain only lowercase letters, numbers, and dashes. Consider renaming it to '$normalized'.",
);
}
}
diff --git a/tests/RenderTest.php b/tests/RenderTest.php
index a551736a..6fd8dcc9 100644
--- a/tests/RenderTest.php
+++ b/tests/RenderTest.php
@@ -112,7 +112,7 @@ public function testBorderRadius(): void
$this->assertStringContainsString("", $render);
$this->assertStringContainsString(
"",
- $render
+ $render,
);
}
@@ -126,19 +126,19 @@ public function testSplitLines(): void
// Check label that is too long, split
$this->assertEquals(
"Chuỗi đóng góp hiệntại",
- splitLines("Chuỗi đóng góp hiện tại", 22, -9)
+ splitLines("Chuỗi đóng góp hiện tại", 22, -9),
);
// Check label with manually inserted line break, split
$this->assertEquals(
"Chuỗi đóng góphiện tại",
- splitLines("Chuỗi đóng góp\nhiện tại", 22, -9)
+ splitLines("Chuỗi đóng góp\nhiện tại", 22, -9),
);
// Check date range label, no split
$this->assertEquals("Mar 28, 2019 – Apr 12, 2019", splitLines("Mar 28, 2019 – Apr 12, 2019", 28, 0));
// Check date range label that is too long, split
$this->assertEquals(
"19 de dez. de 2021- 14 de mar.",
- splitLines("19 de dez. de 2021 - 14 de mar.", 24, 0)
+ splitLines("19 de dez. de 2021 - 14 de mar.", 24, 0),
);
}
@@ -194,7 +194,7 @@ public function testGradientBackground(): void
$this->assertStringContainsString("fill='url(#gradient)'", $render);
$this->assertStringContainsString(
"",
- $render
+ $render,
);
}
@@ -208,7 +208,7 @@ public function testGradientBackgroundWithMoreThan2Colors(): void
$this->assertStringContainsString("fill='url(#gradient)'", $render);
$this->assertStringContainsString(
"",
- $render
+ $render,
);
}
@@ -261,14 +261,13 @@ public function testFirstAndThirdColumnsSwappedWhenDirectionIsRtl(): void
{
$this->testParams["locale"] = "he";
$render = generateOutput($this->testStats, $this->testParams)["body"];
- $renderCollapsedSpaces = preg_replace("/(\s)\s*/", '$1', $render);
- $this->assertStringContainsString(
- "\n",
- $renderCollapsedSpaces
+ $this->assertMatchesRegularExpression(
+ "/\\s*/",
+ $render,
);
- $this->assertStringContainsString(
- "\n",
- $renderCollapsedSpaces
+ $this->assertMatchesRegularExpression(
+ "/\\s*/",
+ $render,
);
}
diff --git a/tests/StatsTest.php b/tests/StatsTest.php
index c05516a7..cef00b08 100644
--- a/tests/StatsTest.php
+++ b/tests/StatsTest.php
@@ -93,6 +93,36 @@ public function testInvalidUsername(): void
getContributionGraphs("help");
}
+ /**
+ * Test that an valid username can be accessed with whitelist
+ */
+ public function testValidUsernameWithWhitelist(): void
+ {
+ $_SERVER["WHITELIST"] = "DenverCoder1";
+ try {
+ $contributionGraphs = getContributionGraphs("DenverCoder1");
+ $this->assertIsArray($contributionGraphs);
+ $this->assertNotEmpty($contributionGraphs);
+ } finally {
+ unset($_SERVER["WHITELIST"]);
+ }
+ }
+
+ /**
+ * Test that an not whitelisted username returns 'not whitelisted' error
+ */
+ public function testNotWhitelistedUsername(): void
+ {
+ $_SERVER["WHITELIST"] = "DenverCoder1";
+ try {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("User not in whitelist.");
+ getContributionGraphs("help");
+ } finally {
+ unset($_SERVER["WHITELIST"]);
+ }
+ }
+
/**
* Test that an organization name returns 'not a user' error
*/