[
  {
    "path": ".deepsource.toml",
    "content": "version = 1\n\ntest_patterns = [\n  \"tests/**\"\n]\n\nexclude_patterns = [\n  \"vendor/**\",\n  \"*.min.js\"\n]\n\n[[analyzers]]\nname = \"php\"\nenabled = true\n\n  [analyzers.meta]\n  skip_doc_coverage = [\"class\", \"magic\"]\n\n[[analyzers]]\nname = \"javascript\"\nenabled = true\n"
  },
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-24.04\n\nADD first-run-notice.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt\n\nRUN apt-get update -y && \\\n    apt-get install -y php php-curl php-xml inkscape composer\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"build\": {\n    \"dockerfile\": \"Dockerfile\"\n  },\n  \"onCreateCommand\": \"/workspaces/github-readme-streak-stats/.devcontainer/on-create.sh\",\n  \"postCreateCommand\": \"/workspaces/github-readme-streak-stats/.devcontainer/post-create.sh\"}\n"
  },
  {
    "path": ".devcontainer/first-run-notice.txt",
    "content": "👋 Welcome to Codespaces! You are using the pre-configured image.\n\nTests can be executed with: composer test\n"
  },
  {
    "path": ".devcontainer/on-create.sh",
    "content": "#!/bin/bash\nset -e\n\ncd /workspaces/github-readme-streak-stats\ncomposer install\n"
  },
  {
    "path": ".devcontainer/post-create.sh",
    "content": "#!/bin/bash\nset -e\n\ncd /workspaces/github-readme-streak-stats\nif [ -n \"$GITHUB_TOKEN\" ]; then\n  echo \"TOKEN=$GITHUB_TOKEN\" > .env\nfi\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\nindent_size = 2\n\n[*.{js,css}]\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [DenverCoder1]\npatreon:\nopen_collective:\nko_fi:\ntidelift:\ncommunity_bridge:\nliberapay:\nissuehunt:\notechie:\ncustom:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"bug\"\nassignees: \"\"\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n\n- Device: [e.g. iPhone6]\n- OS: [e.g. iOS8.1]\n- Browser [e.g. stock browser, safari]\n- Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"enhancement\"\nassignees: \"\"\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: I have a question about GitHub Streak Stats\ntitle: \"\"\nlabels: \"question\"\nassignees: \"\"\n---\n\n**Description**\n\nA brief description of the question or issue:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/theme.md",
    "content": "---\nname: Theme Request\nabout: Request a theme for the project\ntitle: \"\"\nlabels: \"theme\"\nassignees: \"\"\n---\n\n**Describe your theme in detail**\nA clear description about what the theme would entail.\n\n**Include a screenshot / image**\n\n<!-- Optional -->\n\n**Color palette**\nDescribe the colors that could be used with this theme.\n\nAre you going to add the theme?\n\n- [ ] Check for yes\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"composer\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n\n<!-- Please include a summary of the change and which issue is fixed. -->\n\nFixes # <!-- add issue number -->\n\n### Type of change\n\n<!-- Please delete options that are not relevant. -->\n\n- [ ] Bug fix (added a non-breaking change which fixes an issue)\n- [ ] New feature (added a non-breaking change which adds functionality)\n- [ ] Updated documentation (updated the readme, templates, or other repo files)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n\n## How Has This Been Tested?\n\n<!--\nIf you have changed a feature of the stats cards, please describe the tests you made to verify your changes.\nChanges strictly related to documentation can skip this section.\n-->\n\n- [ ] Tested locally with a valid username\n- [ ] Tested locally with an invalid username\n- [ ] Ran tests with `composer test`\n- [ ] Added or updated test cases to test new features\n\n## Checklist:\n\n- [ ] I have checked to make sure no other [pull requests](https://github.com/DenverCoder1/github-readme-streak-stats/pulls?q=is%3Apr+sort%3Aupdated-desc+) are open for this issue\n- [ ] The code is properly formatted and is consistent with the existing code style\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] I have made corresponding changes to the documentation\n- [ ] My changes generate no new warnings\n\n## Screenshots\n\n<!-- If you have updated the design or appearance, please include a screenshot of your changes. -->\n"
  },
  {
    "path": ".github/workflows/force-release.yml",
    "content": "name: Manual Release\n\non:\n  workflow_dispatch:\n\njobs:\n  changelog:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: conventional Changelog Action\n        id: changelog\n        uses: TriPSs/conventional-changelog-action@v3.7.1\n        with:\n          github-token: ${{ secrets.CHANGELOG_RELEASE }}\n          version-file: './composer.json'\n          output-file: 'false'\n          skip-on-empty: 'false'\n\n      - name: create release\n        uses: actions/create-release@v1\n        if: ${{ steps.changelog.outputs.skipped == 'false' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.CHANGELOG_RELEASE }}\n        with:\n          tag_name: ${{ steps.changelog.outputs.tag }}\n          release_name: ${{ steps.changelog.outputs.tag }}\n          body: ${{ steps.changelog.outputs.clean_changelog }}\n"
  },
  {
    "path": ".github/workflows/phpunit-ci-coverage.yml",
    "content": "name: PHPUnit CI\n\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - main\n\nenv:\n  PHP_VERSION: 8.2\n\njobs:\n  build-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: php-actions/composer@v6\n        with:\n          php_extensions: intl\n          php_version: ${{ env.PHP_VERSION }}\n      - name: PHPUnit Tests\n        uses: php-actions/phpunit@v4\n        with:\n          php_extensions: intl\n          php_version: ${{ env.PHP_VERSION }}\n          bootstrap: vendor/autoload.php\n          configuration: tests/phpunit/phpunit.xml\n          args: --testdox\n        env:\n          TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/prettier.yml",
    "content": "name: Format with Prettier\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    paths:\n      - \"**.php\"\n      - \"**.md\"\n      - \"**.js\"\n      - \"**.css\"\n      - \".github/workflows/prettier.yml\"\n\njobs:\n  prettier:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Pull Request\n        if: ${{ github.event_name == 'pull_request' }}\n        uses: actions/checkout@v3\n        with:\n          repository: ${{ github.event.pull_request.head.repo.full_name }}\n          ref: ${{ github.event.pull_request.head.ref }}\n\n      - name: Checkout Push\n        if: ${{ github.event_name != 'pull_request' }}\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: '22'\n\n      - name: Install prettier and plugin-php\n        run: npm i\n\n      - name: Lint with Prettier\n        continue-on-error: true\n        run: composer lint\n\n      - name: Prettify code\n        run: |\n          composer lint-fix\n          git diff\n\n      - name: Commit changes\n        uses: EndBug/add-and-commit@v9\n        with:\n          message: \"style: Formatted code with Prettier\"\n          default_author: github_actions\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Automated Releases\n\non:\n  workflow_dispatch:\n\njobs:\n  changelog:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: conventional Changelog Action\n        id: changelog\n        uses: TriPSs/conventional-changelog-action@v3.7.1\n        with:\n          github-token: ${{ secrets.CHANGELOG_RELEASE }}\n          version-file: './composer.json'\n          output-file: 'false'\n\n      - name: create release\n        uses: actions/create-release@v1\n        if: ${{ steps.changelog.outputs.skipped == 'false' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.CHANGELOG_RELEASE }}\n        with:\n          tag_name: ${{ steps.changelog.outputs.tag }}\n          release_name: ${{ steps.changelog.outputs.tag }}\n          body: ${{ steps.changelog.outputs.clean_changelog }}\n"
  },
  {
    "path": ".github/workflows/translation-progress.yml",
    "content": "name: Update Translation Progress\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths:\n      - \"src/translations.php\"\n      - \"scripts/translation-progress.php\"\n      - \".github/workflows/translation-progress.yml\"\n      - \"README.md\"\n\nenv:\n  PHP_VERSION: 8.2\n\njobs:\n  build-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - uses: php-actions/composer@v6\n        with:\n          php_extensions: intl\n          php_version: ${{ env.PHP_VERSION }}\n  \n      - name: Update Translations\n        run: php scripts/translation-progress.php\n\n      - name: Commit Changes\n        uses: EndBug/add-and-commit@v7\n        with:\n          author_name: GitHub Actions\n          author_email: github-actions[bot]@users.noreply.github.com\n          message: \"docs(readme): Update translation progress\"\n          add: \"README.md\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated files\nvendor/\nnode_modules/\n*.log\ncomposer.phar\nyarn.lock\npackage-lock.json\n.vercel\n\n# Cache directory\ncache/\n\n# Local Configuration\n.DS_Store\n\n# Environment\n.env\n.php-version\nDOCKER_ENV\ndocker_tag\n\n# IDE\n.vscode/\n.idea/\n"
  },
  {
    "path": ".prettierignore",
    "content": "vendor\n**/*.min.js\n.prettierrc"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  printWidth: 120,\n  endOfLine: \"auto\",\n  plugins: [\"@prettier/plugin-php\"],\n  overrides: [\n    {\n      files: \"*.php\",\n      options: {\n        parser: \"php\",\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "Aptfile",
    "content": "inkscape\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n- Using welcoming and inclusive language\n- Being respectful of differing viewpoints and experiences\n- Gracefully accepting constructive criticism\n- Focusing on what is best for the community\n- Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n- The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n- Trolling, insulting/derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting me via direct message on Twitter, Reddit, or Discord. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contributing Guidelines\n\nContributions are welcome! Feel free to open an issue or submit a pull request if you have a way to improve this project.\n\nMake sure your request is meaningful and you have tested the app locally before submitting a pull request.\n\nThis documentation contains a set of guidelines to help you during the contribution process.\n\n### Need some help regarding the basics?\n\nYou can refer to the following articles on the basics of Git and GitHub in case you are stuck:\n\n- [Forking a Repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo)\n- [Cloning a Repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)\n- [How to create a Pull Request](https://opensource.com/article/19/7/create-pull-request-github)\n- [Getting started with Git and GitHub](https://towardsdatascience.com/getting-started-with-git-and-github-6fcd0f2d4ac6)\n- [Learn GitHub from Scratch](https://github.com/githubtraining/introduction-to-github)\n\n### Installing Requirements\n\n#### Requirements\n\n- [PHP 8.2+](https://www.apachefriends.org/index.html)\n- [Composer](https://getcomposer.org)\n- [Inkscape](https://inkscape.org) (for PNG rendering)\n\n#### Linux\n\n```bash\nsudo apt-get install php\nsudo apt-get install php-curl\nsudo apt-get install composer\nsudo apt-get install inkscape\n```\n\n#### Windows\n\nInstall PHP from [XAMPP](https://www.apachefriends.org/index.html) or [php.net](https://windows.php.net/download)\n\n[▶ How to install and run PHP using XAMPP (Windows)](https://www.youtube.com/watch?v=K-qXW9ymeYQ)\n\n[📥 Download Composer](https://getcomposer.org/download/)\n\n### Clone the repository\n\n```\ngit clone https://github.com/DenverCoder1/github-readme-streak-stats.git\ncd github-readme-streak-stats\n```\n\n### Authorization\n\nTo get the GitHub API to run locally you will need to provide a token.\n\n1. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token\n2. Scroll to the bottom and click **\"Generate token\"**\n3. **Make a copy** of the `.env.example` named `.env` in the root directory and add **your token** after `TOKEN=`.\n\n```php\nTOKEN=<your-token>\n```\n\n### Install dependencies\n\nRun the following command to install all the required dependencies to work on this project.\n\n```bash\ncomposer install\n```\n\n### Running the app locally\n\n```bash\ncomposer start\n```\n\nOpen http://localhost:8000/?user=DenverCoder1 to run the project locally\n\nOpen http://localhost:8000/demo/ to run the demo site\n\n### Running the tests\n\nRun the following command to run the PHPUnit test script which will verify that the tested functionality is still working.\n\n```bash\ncomposer test\n```\n\n## Linting\n\nThis project uses Prettier for formatting PHP, Markdown, JavaScript and CSS files.\n\n```bash\n# Run prettier and show the files that need to be fixed\ncomposer lint\n\n# Run prettier and fix the files\ncomposer lint-fix\n```\n\n## Submitting Contributions 👨‍💻\n\nBelow you will find the process and workflow used to review and merge your changes.\n\n### Step 0 : Find an issue\n\n- Take a look at the existing issues or create your **own** issues!\n\n![issues tab](https://user-images.githubusercontent.com/63443481/136185624-24447858-de8d-4b0a-bb6b-2528d9031196.PNG)\n\n### Step 1 : Fork the Project\n\n- Fork this repository. This will create a copy of this repository on your GitHub profile.\n  Keep a reference to the original project in the `upstream` remote.\n\n```bash\ngit clone https://github.com/<your-username>/github-readme-streak-stats.git\ncd github-readme-streak-stats\ngit remote add upstream https://github.com/DenverCoder1/github-readme-streak-stats.git\n```\n\n![fork button](https://user-images.githubusercontent.com/63443481/136185816-0b6770d7-0b00-4951-861a-dd15e3954918.PNG)\n\n- If you have already forked the project, update your copy before working.\n\n```bash\ngit remote update\ngit checkout <branch-name>\ngit rebase upstream/<branch-name>\n```\n\n### Step 2 : Branch\n\nCreate a new branch. Use its name to identify the issue you're addressing.\n\n```bash\n# Creates a new branch with the name feature_name and switches to it\ngit checkout -b feature_name\n```\n\n### Step 3 : Work on the issue assigned\n\n- Work on the issue(s) assigned to you.\n- Make all the necessary changes to the codebase.\n- After you've made changes or made your contribution to the project, add changes to the branch you've just created using:\n\n```bash\n# To add all new files to the branch\ngit add .\n\n# To add only a few files to the branch\ngit add <some files (with path)>\n```\n\n### Step 4 : Commit\n\n- Commit a descriptive message using:\n\n```bash\n# This message will get associated with all files you have changed\ngit commit -m \"message\"\n```\n\n### Step 5 : Work Remotely\n\n- Now you are ready to your work on the remote repository.\n- When your work is ready and complies with the project conventions, upload your changes to your fork:\n\n```bash\n# To push your work to your remote repository\ngit push -u origin Branch_Name\n```\n\n- Here is how your branch will look.\n\n  ![forked branch](https://user-images.githubusercontent.com/63443481/136186235-204f5c7a-1129-44b5-af20-89aa6a68d952.PNG)\n\n### Step 6 : Pull Request\n\n- Go to your forked repository in your browser and click on \"Compare and pull request\". Then add a title and description to your pull request that explains your contribution.\n\n<img width=\"700\" alt=\"compare and pull request\" src=\"https://user-images.githubusercontent.com/63443481/136186304-c0a767ea-1fd2-4b0c-b5a8-3e366ddc06a3.PNG\">\n\n<img width=\"882\" alt=\"opening pull request\" src=\"https://user-images.githubusercontent.com/63443481/136186322-bfd5f333-136a-4d2f-8891-e8f97c379ba8.PNG\">\n\n- Voila! Your Pull Request has been submitted and it's ready to be merged.🥳\n\n#### Happy Contributing!\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use PHP 8.3 (8.4 not supported yet)\nFROM php:8.3-apache@sha256:6be4ef702b2dd05352f7e5fe14667696a4ad091c9d2ad9083becbee4300dc3b1\n\n# Install system dependencies and PHP extensions in one layer\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    git \\\n    unzip \\\n    libicu-dev \\\n    inkscape \\\n    fonts-dejavu-core \\\n    curl \\\n    && docker-php-ext-configure intl \\\n    && docker-php-ext-install intl \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Install Composer\nCOPY --from=composer/composer:latest-bin@sha256:c9bda63056674836406cacfbbdd8ef770fb4692ac419c967034225213c64e11b /composer /usr/bin/composer\n\n# Set working directory\nWORKDIR /var/www/html\n\n# Copy composer files and install dependencies\nCOPY composer.json composer.lock ./\nCOPY src/ ./src/\nRUN composer install --no-dev --optimize-autoloader --no-scripts\n\n# Configure Apache to serve from src/ directory and pass environment variables\nRUN a2enmod rewrite headers && \\\n    echo 'ServerTokens Prod\\n\\\n    ServerSignature Off\\n\\\n    PassEnv TOKEN\\n\\\n    PassEnv WHITELIST\\n\\\n    <VirtualHost *:80>\\n\\\n    ServerAdmin webmaster@localhost\\n\\\n    DocumentRoot /var/www/html/src\\n\\\n    <Directory /var/www/html/src>\\n\\\n    Options -Indexes\\n\\\n    AllowOverride None\\n\\\n    Require all granted\\n\\\n    Header always set Access-Control-Allow-Origin \"*\"\\n\\\n    Header always set Content-Type \"image/svg+xml\" \"expr=%{REQUEST_URI} =~ m#\\\\.svg$#i\"\\n\\\n    Header always set Content-Security-Policy \"default-src 'none'; style-src 'unsafe-inline'; img-src data:;\" \"expr=%{REQUEST_URI} =~ m#\\\\.svg$#i\"\\n\\\n    Header always set Referrer-Policy \"no-referrer-when-downgrade\"\\n\\\n    Header always set X-Content-Type-Options \"nosniff\"\\n\\\n    </Directory>\\n\\\n    ErrorLog ${APACHE_LOG_DIR}/error.log\\n\\\n    CustomLog ${APACHE_LOG_DIR}/access.log combined\\n\\\n    </VirtualHost>' > /etc/apache2/sites-available/000-default.conf\n\nRUN mkdir -p /var/www/html/cache\n\n# Set secure permissions (cache dir needs write access for www-data)\nRUN chown -R www-data:www-data /var/www/html && \\\n    find /var/www/html -type d -exec chmod 755 {} \\; && \\\n    find /var/www/html -type f -exec chmod 644 {} \\; && \\\n    chmod 775 /var/www/html/cache\n\n# Health check\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n    CMD curl -f http://localhost/demo/ || exit 1\n\n# Expose port\nEXPOSE 80\n\n# Start Apache\nCMD [\"apache2-foreground\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Jonah Lawrence\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Procfile",
    "content": "web: vendor/bin/heroku-php-apache2 src/\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://i.imgur.com/GZHodUG.png\" width=\"100px\"/>\n  <h3 align=\"center\">Github Readme Streak Stats</h3>\n</p>\n\n<p align=\"center\">\n  Display your total contributions, current streak,\n  <br/>\n  and longest streak on your GitHub profile README\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/search?q=extension%3Amd+%22github+readme+streak+stats+herokuapp%22&type=Code\" alt=\"Users\" title=\"Repo users\">\n    <img src=\"https://freshidea.com/jonah/app/github-search-results/streak-stats\"/></a>\n  <a href=\"https://discord.gg/fPrdqh3Zfu\" alt=\"Discord\" title=\"Dev Pro Tips Discussion & Support Server\">\n    <img src=\"https://img.shields.io/discord/819650821314052106?color=7289DA&logo=discord&logoColor=white&style=for-the-badge\"/></a>\n</p>\n\n## ⚡ Quick setup\n\n1. Copy-paste the markdown below into your GitHub profile README\n2. Replace the value after `?user=` with your GitHub username\n\n```md\n[![GitHub Streak](https://streak-stats.demolab.com/?user=DenverCoder1)](https://git.io/streak-stats)\n```\n\n3. Star the repo 😄\n\n### Next Steps\n\n- Check out the [Demo Site](https://streak-stats.demolab.com) or [Options](https://github.com/DenverCoder1/github-readme-streak-stats?tab=readme-ov-file#-options) below for available customizations.\n\n- It is recommended to self-host the project more better reliability. See [Deploying it on your own](https://github.com/DenverCoder1/github-readme-streak-stats?tab=readme-ov-file#-deploying-it-on-your-own) for more details.\n\n[![][hspace]](#) [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)][herokudeploy] [![Deploy to Vercel](https://i.imgur.com/Mb3VLCi.png)][verceldeploy]\n\n## ⚙ Demo Site\n\nHere you can customize your Streak Stats card with a live preview.\n\n<https://streak-stats.demolab.com>\n\n[![Demo Site](https://user-images.githubusercontent.com/20955511/114579753-dbac8780-9c86-11eb-97dd-207039f67d20.gif \"Demo Site\")](http://streak-stats.demolab.com/demo/)\n\n## 🔧 Options\n\nThe `user` field is the only required option. All other fields are optional.\n\nIf the `theme` parameter is specified, any color customizations specified will be applied on top of the theme, overriding the theme's values.\n\n|         Parameter          |                     Details                      |                                              Example                                               |\n| :------------------------: | :----------------------------------------------: | :------------------------------------------------------------------------------------------------: |\n|           `user`           |        GitHub username to show stats for         |                                           `DenverCoder1`                                           |\n|          `theme`           |     The theme to apply (Default: `default`)      |                          `dark`, `radical`, etc. [🎨➜](./docs/themes.md)                           |\n|       `hide_border`        |  Make the border transparent (Default: `false`)  |                                         `true` or `false`                                          |\n|      `border_radius`       | Set the roundness of the edges (Default: `4.5`)  |                           Number `0` (sharp corners) to `248` (ellipse)                            |\n|        `background`        |  Background color (eg. `f2f2f2`, `35,d22,00f`)   | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |\n|          `border`          |                   Border color                   |                             **hex code** without `#` or **css color**                              |\n|          `stroke`          |        Stroke line color between sections        |                             **hex code** without `#` or **css color**                              |\n|           `ring`           |   Color of the ring around the current streak    |                             **hex code** without `#` or **css color**                              |\n|           `fire`           |          Color of the fire in the ring           |                             **hex code** without `#` or **css color**                              |\n|      `currStreakNum`       |              Current streak number               |                             **hex code** without `#` or **css color**                              |\n|         `sideNums`         |         Total and longest streak numbers         |                             **hex code** without `#` or **css color**                              |\n|     `currStreakLabel`      |               Current streak label               |                             **hex code** without `#` or **css color**                              |\n|        `sideLabels`        |         Total and longest streak labels          |                             **hex code** without `#` or **css color**                              |\n|          `dates`           |              Date range text color               |                             **hex code** without `#` or **css color**                              |\n|     `excludeDaysLabel`     |       Excluded days of the week text color       |                             **hex code** without `#` or **css color**                              |\n|       `date_format`        |  Date format pattern or empty for locale format  |                        See note below on [📅 Date Formats](#-date-formats)                         |\n|          `locale`          |  Locale for labels and numbers (Default: `en`)   |                            ISO 639-1 code - See [🗪 Locales](#-locales)                             |\n|      `short_numbers`       |  Use short numbers (e.g. 1.5k instead of 1,500)  |                                         `true` or `false`                                          |\n|           `type`           |          Output format (Default: `svg`)          |                              Current options: `svg`, `png` or `json`                               |\n|           `mode`           |          Streak mode (Default: `daily`)          |             `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week)              |\n|       `exclude_days`       | List of days of the week to exclude from streaks |    Comma-separated list of day abbreviations (Sun, Mon, Tue, Wed, Thu, Fri, Sat) e.g. `Sun,Sat`    |\n|    `disable_animations`    |    Disable SVG animations (Default: `false`)     |                                         `true` or `false`                                          |\n|        `card_width`        |   Width of the card in pixels (Default: `495`)   |                        Positive integer, minimum width is 100px per column                         |\n|       `card_height`        |  Height of the card in pixels (Default: `195`)   |                             Positive integer, minimum height is 170px                              |\n| `hide_total_contributions` | Hide the total contributions (Default: `false`)  |                                         `true` or `false`                                          |\n|   `hide_current_streak`    |    Hide the current streak (Default: `false`)    |                                         `true` or `false`                                          |\n|   `hide_longest_streak`    |    Hide the longest streak (Default: `false`)    |                                         `true` or `false`                                          |\n|      `starting_year`       |          Starting year of contributions          |   Integer, must be `2005` or later, eg. `2017`. By default, your account creation year is used.    |\n\n### 🖌 Themes\n\nTo enable a theme, append `&theme=` followed by the theme name to the end of the source URL:\n\n```md\n[![GitHub Streak](https://streak-stats.demolab.com/?user=DenverCoder1&theme=dark)](https://git.io/streak-stats)\n```\n\n|     Theme      |                            Preview                            |\n| :------------: | :-----------------------------------------------------------: |\n|   `default`    |          ![default](https://i.imgur.com/IaTuYdS.png)          |\n|     `dark`     |           ![dark](https://i.imgur.com/bUrsjlp.png)            |\n| `highcontrast` |       ![highcontrast](https://i.imgur.com/ovrVrTY.png)        |\n|  More themes!  | **🎨 [See a list of all available themes](./docs/themes.md)** |\n\n**If you have come up with a new theme you'd like to share with others, please see [Issue #32](https://github.com/DenverCoder1/github-readme-streak-stats/issues/32) for more information on how to contribute.**\n\n### 🗪 Locales\n\nThe following are the locales that have labels translated in Streak Stats. The `locale` query parameter accepts any ISO language or locale code, see [here](https://gist.github.com/DenverCoder1/f61147ba26bfcf7c3bf605af7d3382d5) for a list of valid locales. The locale provided will be used for the date format and number format even if translations are not yet available.\n\n<!-- This section is automatically generated by the `translation-progress.php` script. -->\n<!-- prettier-ignore-start -->\n<!-- TRANSLATION_PROGRESS_START -->\n<table><tbody><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L37\"><code>en</code></a> - English<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L37\"><img src=\"https://progress-bar.xyz/100\" alt=\"English 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L47\"><code>am</code></a> - አማርኛ<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L47\"><img src=\"https://progress-bar.xyz/100\" alt=\"አማርኛ 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L56\"><code>ar</code></a> - العربية<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L56\"><img src=\"https://progress-bar.xyz/100\" alt=\"العربية 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L67\"><code>as</code></a> - অসমীয়া<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L67\"><img src=\"https://progress-bar.xyz/100\" alt=\"অসমীয়া 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L84\"><code>bho</code></a> - भोजपुरी<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L84\"><img src=\"https://progress-bar.xyz/100\" alt=\"भोजपुरी 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L93\"><code>bn</code></a> - বাংলা<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L93\"><img src=\"https://progress-bar.xyz/100\" alt=\"বাংলা 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L102\"><code>ca</code></a> - català<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L102\"><img src=\"https://progress-bar.xyz/100\" alt=\"català 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L111\"><code>ceb</code></a> - Cebuano<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L111\"><img src=\"https://progress-bar.xyz/100\" alt=\"Cebuano 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L120\"><code>da</code></a> - dansk<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L120\"><img src=\"https://progress-bar.xyz/100\" alt=\"dansk 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L129\"><code>de</code></a> - Deutsch<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L129\"><img src=\"https://progress-bar.xyz/100\" alt=\"Deutsch 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L138\"><code>el</code></a> - Ελληνικά<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L138\"><img src=\"https://progress-bar.xyz/100\" alt=\"Ελληνικά 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L147\"><code>es</code></a> - español<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L147\"><img src=\"https://progress-bar.xyz/100\" alt=\"español 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L156\"><code>fa</code></a> - فارسی<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L156\"><img src=\"https://progress-bar.xyz/100\" alt=\"فارسی 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L167\"><code>fil</code></a> - Filipino<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L167\"><img src=\"https://progress-bar.xyz/100\" alt=\"Filipino 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L176\"><code>fr</code></a> - français<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L176\"><img src=\"https://progress-bar.xyz/100\" alt=\"français 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L185\"><code>gu</code></a> - ગુજરાતી<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L185\"><img src=\"https://progress-bar.xyz/100\" alt=\"ગુજરાતી 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L194\"><code>he</code></a> - עברית<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L194\"><img src=\"https://progress-bar.xyz/100\" alt=\"עברית 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L204\"><code>hi</code></a> - हिन्दी<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L204\"><img src=\"https://progress-bar.xyz/100\" alt=\"हिन्दी 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L221\"><code>hu</code></a> - magyar<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L221\"><img src=\"https://progress-bar.xyz/100\" alt=\"magyar 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L238\"><code>id</code></a> - Indonesia<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L238\"><img src=\"https://progress-bar.xyz/100\" alt=\"Indonesia 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L247\"><code>it</code></a> - italiano<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L247\"><img src=\"https://progress-bar.xyz/100\" alt=\"italiano 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L256\"><code>ja</code></a> - 日本語<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L256\"><img src=\"https://progress-bar.xyz/100\" alt=\"日本語 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L267\"><code>jv</code></a> - Jawa<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L267\"><img src=\"https://progress-bar.xyz/100\" alt=\"Jawa 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L276\"><code>kn</code></a> - ಕನ್ನಡ<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L276\"><img src=\"https://progress-bar.xyz/100\" alt=\"ಕನ್ನಡ 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L285\"><code>ko</code></a> - 한국어<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L285\"><img src=\"https://progress-bar.xyz/100\" alt=\"한국어 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L294\"><code>mai</code></a> - मैथिली<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L294\"><img src=\"https://progress-bar.xyz/100\" alt=\"मैथिली 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L303\"><code>mal</code></a> - മലയാളം<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L303\"><img src=\"https://progress-bar.xyz/100\" alt=\"മലയാളം 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L313\"><code>mi</code></a> - Māori<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L313\"><img src=\"https://progress-bar.xyz/100\" alt=\"Māori 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L322\"><code>mr</code></a> - मराठी<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L322\"><img src=\"https://progress-bar.xyz/100\" alt=\"मराठी 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L331\"><code>ms</code></a> - Melayu<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L331\"><img src=\"https://progress-bar.xyz/100\" alt=\"Melayu 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L340\"><code>ms_ID</code></a> - Melayu (Indonesia)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L340\"><img src=\"https://progress-bar.xyz/100\" alt=\"Melayu (Indonesia) 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L349\"><code>my</code></a> - မြန်မာ<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L349\"><img src=\"https://progress-bar.xyz/100\" alt=\"မြန်မာ 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L358\"><code>ne</code></a> - नेपाली<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L358\"><img src=\"https://progress-bar.xyz/100\" alt=\"नेपाली 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L367\"><code>nl</code></a> - Nederlands<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L367\"><img src=\"https://progress-bar.xyz/100\" alt=\"Nederlands 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L376\"><code>no</code></a> - norsk<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L376\"><img src=\"https://progress-bar.xyz/100\" alt=\"norsk 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L385\"><code>pa</code></a> - ਪੰਜਾਬੀ<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L385\"><img src=\"https://progress-bar.xyz/100\" alt=\"ਪੰਜਾਬੀ 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L394\"><code>pl</code></a> - polski<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L394\"><img src=\"https://progress-bar.xyz/100\" alt=\"polski 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L403\"><code>ps</code></a> - پښتو<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L403\"><img src=\"https://progress-bar.xyz/100\" alt=\"پښتو 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L414\"><code>pt</code></a> - português<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L414\"><img src=\"https://progress-bar.xyz/100\" alt=\"português 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L423\"><code>pt_BR</code></a> - português (Brasil)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L423\"><img src=\"https://progress-bar.xyz/100\" alt=\"português (Brasil) 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L432\"><code>ro</code></a> - română<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L432\"><img src=\"https://progress-bar.xyz/100\" alt=\"română 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L441\"><code>ru</code></a> - русский<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L441\"><img src=\"https://progress-bar.xyz/100\" alt=\"русский 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L458\"><code>sa</code></a> - संस्कृत भाषा<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L458\"><img src=\"https://progress-bar.xyz/100\" alt=\"संस्कृत भाषा 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L467\"><code>sd_PK</code></a> - سنڌي (پاڪستان)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L467\"><img src=\"https://progress-bar.xyz/100\" alt=\"سنڌي (پاڪستان) 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L479\"><code>sr_Cyrl</code></a> - српски (ћирилица)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L479\"><img src=\"https://progress-bar.xyz/100\" alt=\"српски (ћирилица) 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L488\"><code>sr_Latn</code></a> - srpski (latinica)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L488\"><img src=\"https://progress-bar.xyz/100\" alt=\"srpski (latinica) 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L497\"><code>su</code></a> - Basa Sunda<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L497\"><img src=\"https://progress-bar.xyz/100\" alt=\"Basa Sunda 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L506\"><code>sv</code></a> - svenska<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L506\"><img src=\"https://progress-bar.xyz/100\" alt=\"svenska 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L515\"><code>sw</code></a> - Kiswahili<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L515\"><img src=\"https://progress-bar.xyz/100\" alt=\"Kiswahili 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L524\"><code>ta</code></a> - தமிழ்<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L524\"><img src=\"https://progress-bar.xyz/100\" alt=\"தமிழ் 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L533\"><code>tcy</code></a> - Tulu<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L533\"><img src=\"https://progress-bar.xyz/100\" alt=\"Tulu 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L542\"><code>te</code></a> - తెలుగు<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L542\"><img src=\"https://progress-bar.xyz/100\" alt=\"తెలుగు 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L551\"><code>th</code></a> - ไทย<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L551\"><img src=\"https://progress-bar.xyz/100\" alt=\"ไทย 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L560\"><code>tr</code></a> - Türkçe<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L560\"><img src=\"https://progress-bar.xyz/100\" alt=\"Türkçe 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L569\"><code>uk</code></a> - українська<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L569\"><img src=\"https://progress-bar.xyz/100\" alt=\"українська 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L578\"><code>ur_PK</code></a> - اردو (پاکستان)<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L578\"><img src=\"https://progress-bar.xyz/100\" alt=\"اردو (پاکستان) 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L589\"><code>vi</code></a> - Tiếng Việt<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L589\"><img src=\"https://progress-bar.xyz/100\" alt=\"Tiếng Việt 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L598\"><code>yo</code></a> - Èdè Yorùbá<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L598\"><img src=\"https://progress-bar.xyz/100\" alt=\"Èdè Yorùbá 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L608\"><code>zh_Hans</code></a> - 中文（简体）<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L608\"><img src=\"https://progress-bar.xyz/100\" alt=\"中文（简体） 100%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L618\"><code>zh_Hant</code></a> - 中文（繁體）<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L618\"><img src=\"https://progress-bar.xyz/100\" alt=\"中文（繁體） 100%\"></a></td></tr><tr><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L76\"><code>bg</code></a> - български<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L76\"><img src=\"https://progress-bar.xyz/86\" alt=\"български 86%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L213\"><code>ht</code></a> - créole haïtien<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L213\"><img src=\"https://progress-bar.xyz/86\" alt=\"créole haïtien 86%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L230\"><code>hy</code></a> - հայերեն<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L230\"><img src=\"https://progress-bar.xyz/86\" alt=\"հայերեն 86%\"></a></td><td><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L450\"><code>rw</code></a> - Kinyarwanda<br /><a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L450\"><img src=\"https://progress-bar.xyz/86\" alt=\"Kinyarwanda 86%\"></a></td><td></td></tr></tbody></table>\n<!-- TRANSLATION_PROGRESS_END -->\n<!-- prettier-ignore-end -->\n\n**If you would like to help translate the Streak Stats cards, please see [Issue #236](https://github.com/DenverCoder1/github-readme-streak-stats/issues/236) for more information.**\n\n### 📅 Date Formats\n\nIf `date_format` is not provided or is empty, the PHP Intl library is used to determine the date format based on the locale specified in the `locale` query parameter.\n\nA custom date format can be specified by passing a string to the `date_format` parameter.\n\nThe required format is to use format string characters from [PHP's date function](https://www.php.net/manual/en/datetime.format.php) with brackets around the part representing the year.\n\nWhen the contribution year is equal to the current year, the characters in brackets will be omitted.\n\n**Examples:**\n\n|     Date Format     |                                     Result                                      |\n| :-----------------: | :-----------------------------------------------------------------------------: |\n| <pre>d F[, Y]</pre> | <pre>\"2020-04-14\" => \"14 April, 2020\"<br/><br/>\"2024-04-14\" => \"14 April\"</pre> |\n|  <pre>j/n/Y</pre>   |   <pre>\"2020-04-14\" => \"14/4/2020\"<br/><br/>\"2024-04-14\" => \"14/4/2024\"</pre>   |\n| <pre>[Y.]n.j</pre>  |     <pre>\"2020-04-14\" => \"2020.4.14\"<br/><br/>\"2024-04-14\" => \"4.14\"</pre>      |\n| <pre>M j[, Y]</pre> |   <pre>\"2020-04-14\" => \"Apr 14, 2020\"<br/><br/>\"2024-04-14\" => \"Apr 14\"</pre>   |\n\n### Example\n\n```md\n[![GitHub Streak](https://streak-stats.demolab.com/?user=denvercoder1&currStreakNum=2FD3EB&fire=pink&sideLabels=F00&date_format=[Y.]n.j)](https://git.io/streak-stats)\n```\n\n## ℹ️ How these stats are calculated\n\nThis tool uses the contribution graphs on your GitHub profile to calculate which days you have contributed.\n\nTo include contributions in private repositories, turn on the setting for \"Private contributions\" from the dropdown menu above the contribution graph on your profile page.\n\nContributions include commits, pull requests, and issues that you create in standalone repositories.\n\nThe longest streak is the highest number of consecutive days on which you have made at least one contribution.\n\nThe 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.\n\n> [!NOTE]\n> 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))\n\n## 📤 Deploying it on your own\n\nIt is preferable to host the files on your own server and it takes less than 2 minutes to set up.\n\nDoing this can lead to better uptime and more control over customization (you can modify the code for your usage).\n\nYou can deploy the PHP files on any website server with PHP installed including Heroku and Vercel.\n\nThe Inkscape dependency is required for PNG rendering, as well as Segoe UI font for the intended rendering. If using Heroku, the buildpacks will install these for you automatically.\n\n### [![Deploy to Vercel](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/5a503e6b-c462-4627-82ee-651f2cb2a1fc)][verceldeploy]\n\nVercel 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.\n\n> [!NOTE]\n> PNG mode is not supported since Inkscape will not be installed but the default SVG mode will work.\n\n### 📺 [Click here for a video tutorial on how to self-host on Vercel](https://www.youtube.com/watch?v=maoXtlb8t44)\n\n<details>\n  <summary><b>Instructions for deploying to Vercel (Free)</b></summary>\n\n### Step-by-step instructions for deploying to Vercel\n\n#### Option 1: Deploy to Vercel quickly with the Deploy button (recommended)\n\n> [!IMPORTANT]\n> Make sure that you host the **`vercel`** branch as otherwise you'll get a 404 error from Vercel. You can set the `vercel` branch as default after forking the repo.\n\n1. Click the Deploy button below\n\n[![][hspace]](#) [![Deploy with Vercel](https://i.imgur.com/Mb3VLCi.png)][verceldeploy]\n\n2. Create your repository by filling in a Repository Name and clicking \"Create\"\n3. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token (no scopes required)\n4. Scroll to the bottom and click **\"Generate token\"**\n5. **Add the token** as a Config Var with the key `TOKEN`:\n\n![vercel environment variables](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/17a433d6-0aaa-4c69-9a53-6d4638318fbb)\n\n6. Click **\"Deploy\"** at the end of the form\n7. Once the app is deployed, click the screenshot of your app or continue to the dashboard to find your domain to use in place of `streak-stats.demolab.com`\n\n![deployment](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/32092461-5983-4fed-b21b-29be55ed85e8)\n\n> ⚠️ **Note**\n> 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.\n>\n> ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/5fb18fb5-debe-4620-9c8b-193ab442a617)\n\n#### Option 2: Deploy to Vercel manually\n\n1. Sign in to **Vercel** or create a new account at <https://vercel.com>\n2. Use the following command to clone the repository: `git clone https://github.com/DenverCoder1/github-readme-streak-stats.git`. If you plan to make changes, you can also fork the repository and clone your fork instead. If you do not have Git installed, you can download it from <https://git-scm.com/downloads>.\n3. Navigate to the cloned repository's directory using the command `cd github-readme-streak-stats`\n4. Switch to the \"vercel\" branch using the command `git checkout vercel`\n5. Make sure you have the Vercel CLI (Command Line Interface) installed on your system. If not, you can download it from <https://vercel.com/download>.\n6. Run the command `vercel` and follow the prompts to link your Vercel account and choose a project name\n7. After successful deployment, your app will be available at `<project-name>.vercel.app`\n8. Open [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token on GitHub. You don't need to select any scopes for the token.\n9. Scroll to the bottom of the page and click on **\"Generate token\"**\n10. Visit the Vercel dashboard at <https://vercel.com/dashboard> and select your project. Then, click on **\"Settings\"** and choose **\"Environment Variables\"**.\n11. Add a new environment variable with the key `TOKEN` and the value as the token you generated in step 9, then save your changes\n12. (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.\n13. To apply the new environment variable(s), you need to redeploy the app. Run `vercel --prod` to deploy the app to production.\n\n![image](https://user-images.githubusercontent.com/20955511/209588756-8bf5b0cd-9aa6-41e8-909c-97bf41e525b3.png)\n\n> ⚠️ **Note**\n> 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.\n\n> ⚠️ **Note**\n> 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.\n>\n> ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/5fb18fb5-debe-4620-9c8b-193ab442a617)\n\n</details>\n\n### [![Deploy on Heroku](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/e8b575af-5746-4200-a295-7e7baa448383)][herokudeploy]\n\nHeroku is another great option for hosting the files. All features are supported on Heroku and it is where the default domain is hosted. Heroku is not free, however, and you will need to pay between \\$5 and \\$7 per month to keep the app running. Expand the instructions below to learn how to deploy to Heroku.\n\n<details>\n  <summary><b>Instructions for deploying to Heroku (Paid)</b></summary>\n\n### Step-by-step instructions for deploying to Heroku\n\n1. Sign in to **Heroku** or create a new account at <https://heroku.com>\n2. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token (no scopes required)\n3. Scroll to the bottom and click **\"Generate token\"**\n4. Click the Deploy button below\n\n[![][hspace]](#) [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)][herokudeploy]\n\n5. **Add the token** as a Config Var with the key `TOKEN`:\n\n![heroku config variables](https://user-images.githubusercontent.com/20955511/136292022-a8d9b3b5-d7d8-4a5e-a049-8d23b51ce9d7.png)\n\n6. (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.\n7. Click **\"Deploy App\"** at the end of the form\n8. Once the app is deployed, you can use `<your-app-name>.herokuapp.com` in place of `streak-stats.demolab.com`\n\n</details>\n\n### ![Deploy on your own](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/e36ed842-ab56-473a-83fd-ace5bf968996)\n\nYou can transfer the files to any webserver using FTP or other means, then refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for installation steps.\n\n### 🐳 Docker\n\nDocker 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.\n\n<details>\n  <summary><b>Instructions for deploying with Docker</b></summary>\n\n### Step-by-step instructions for deploying with Docker\n\n1. Clone the repository:\n\n   ```bash\n   git clone https://github.com/DenverCoder1/github-readme-streak-stats.git\n   cd github-readme-streak-stats\n   ```\n\n2. Visit https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats to create a new Personal Access Token (no scopes required)\n\n3. Scroll to the bottom and click \"Generate token\"\n\n4. Build the Docker image:\n\n   ```bash\n   docker build -t streak-stats .\n   ```\n\n5. Run the container with your GitHub token:\n\n   ```bash\n   docker run -d -p 8080:80 -e TOKEN=your_github_token_here streak-stats\n   ```\n\n6. 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.\n   Provide the usernames as a comma-separated list, for example:\n\n   ```bash\n   docker run -d -p 8080:80 -e TOKEN=your_github_token_here -e WHITELIST=user1,user2,user3 streak-stats\n   ```\n\n7. Visit http://localhost:8080 to access your self-hosted instance\n\n</details>\n\n[hspace]: https://user-images.githubusercontent.com/20955511/136058102-b79570bc-4912-4369-b664-064a0ada8588.png\n[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\n[herokudeploy]: https://heroku.com/deploy?template=https://github.com/DenverCoder1/github-readme-streak-stats/tree/main\n\n## 🤗 Contributing\n\nContributions are welcome! Feel free to [open an issue](https://github.com/DenverCoder1/github-readme-streak-stats/issues/new/choose) or submit a [pull request](https://github.com/DenverCoder1/github-readme-streak-stats/compare) if you have a way to improve this project.\n\nMake sure your request is meaningful and you have tested the app locally before submitting a pull request.\n\nRefer to [CONTRIBUTING.md](/CONTRIBUTING.md) for more details on contributing, installing requirements, and running the application.\n\n## 🙋‍♂️ Support\n\n💙 If you like this project, give it a ⭐ and share it with friends!\n\n<p align=\"left\">\n  <a href=\"https://www.youtube.com/channel/UCipSxT7a3rn81vGLw9lqRkg?sub_confirmation=1\"><img alt=\"Youtube\" title=\"Youtube\" src=\"https://img.shields.io/badge/-Subscribe-red?style=for-the-badge&logo=youtube&logoColor=white\"/></a>\n  <a href=\"https://github.com/sponsors/DenverCoder1\"><img alt=\"Sponsor with Github\" title=\"Sponsor with Github\" src=\"https://img.shields.io/badge/-Sponsor-ea4aaa?style=for-the-badge&logo=github&logoColor=white\"/></a>\n</p>\n\n[☕ Buy me a coffee](https://ko-fi.com/jlawrence)\n\n---\n\nMade with ❤️ and PHP\n\n<a href=\"https://heroku.com/\"><img alt=\"Powered by Heroku\" title=\"Powered by Heroku\" src=\"https://img.shields.io/badge/-Powered%20by%20Heroku-6567a5?style=for-the-badge&logo=heroku&logoColor=white\"/></a>\n"
  },
  {
    "path": "app.json",
    "content": "{\n  \"name\": \"GitHub Readme Streak Stats\",\n  \"description\": \"🔥 Stay motivated and show off your contribution streak! 🌟 Display your total contributions, current streak, and longest streak on your GitHub profile README.\",\n  \"repository\": \"https://github.com/DenverCoder1/github-readme-streak-stats/\",\n  \"logo\": \"https://i.imgur.com/Z4bDOxC.png\",\n  \"keywords\": [\"github\", \"dynamic\", \"readme\", \"contributions\", \"streak\", \"stats\"],\n  \"addons\": [],\n  \"env\": {\n    \"TOKEN\": {\n      \"description\": \"GitHub personal access token obtained from https://github.com/settings/tokens/new\",\n      \"required\": true\n    }\n  },\n  \"formation\": {\n    \"web\": {\n      \"quantity\": 1,\n      \"size\": \"basic\"\n    }\n  },\n  \"buildpacks\": [\n    {\n      \"url\": \"https://github.com/heroku/heroku-buildpack-apt\"\n    },\n    {\n      \"url\": \"https://github.com/DenverCoder1/heroku-buildpack-fonts-segoe-ui\"\n    },\n    {\n      \"url\": \"heroku/php\"\n    }\n  ]\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"denvercoder1/github-readme-streak-stats\",\n  \"description\": \"🔥 Stay motivated and show off your contribution streak! 🌟 Display your total contributions, current streak, and longest streak on your GitHub profile README.\",\n  \"keywords\": [\n    \"github\",\n    \"dynamic\",\n    \"readme\",\n    \"contributions\",\n    \"streak\",\n    \"stats\"\n  ],\n  \"license\": \"MIT\",\n  \"version\": \"1.6.0\",\n  \"homepage\": \"https://github.com/DenverCoder1/github-readme-streak-stats\",\n  \"repositories\": [\n      {\n          \"type\": \"vcs\",\n          \"url\": \"https://github.com/DenverCoder1/github-readme-streak-stats\"\n      }\n  ],\n  \"support\": {\n    \"issues\": \"https://github.com/DenverCoder1/github-readme-streak-stats/issues\",\n    \"source\": \"https://github.com/DenverCoder1/github-readme-streak-stats\"\n  },\n  \"autoload\": {\n    \"classmap\": [\n      \"src/\"\n    ]\n  },\n  \"require\": {\n    \"php\": \"^8.2\",\n    \"ext-intl\": \"*\",\n    \"vlucas/phpdotenv\": \"^5.3\"\n  },\n  \"require-dev\": {\n    \"phpunit/phpunit\": \"^11\"\n  },\n  \"scripts\": {\n    \"start\": [\n      \"Composer\\\\Config::disableProcessTimeout\",\n      \"php -S localhost:8000 -t src\"\n    ],\n    \"test\": \"./vendor/bin/phpunit --testdox tests\",\n    \"lint\": \"npx prettier --check *.md **/*.{php,md,js,css}\",\n    \"lint-fix\": \"npx prettier --write *.md **/*.{php,md,js,css}\"\n  }\n}\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# FAQ\n\n## How do I create a Readme for my profile?\n\nA profile readme appears on your profile page when you create a repository with the same name as your username and add a `README.md` file to it. For example, the repository for the user [`DenverCoder1`](https://github.com/DenverCoder1) is located at [`DenverCoder1/DenverCoder1`](https://github.com/DenverCoder1/DenverCoder1).\n\n## How do I include GitHub Readme Streak Stats in my Readme?\n\nMarkdown files on GitHub support embedded images using Markdown or HTML. You can customize your Streak Stats image on the [demo site](https://streak-stats.demolab.com/demo/) and use the image source in either of the following ways:\n\n### Markdown\n\n```md\n[![GitHub Streak](https://streak-stats.demolab.com?user=DenverCoder1)](https://git.io/streak-stats)\n```\n\n### HTML\n\n<!-- prettier-ignore-start -->\n```html\n<a href=\"https://git.io/streak-stats\"><img src=\"https://streak-stats.demolab.com?user=DenverCoder1\"/></a>\n```\n<!-- prettier-ignore-end -->\n\n## Why doesn't my Streak Stats match my contribution graph?\n\nGitHub Readme Streak Stats uses the GitHub API to fetch your contribution data. These stats are returned in UTC time which may not match your local time. Additionally, due to caching, the stats may not be updated immediately after a commit. You may need to wait up to a few hours to see the latest stats.\n\nIf you think your stats are not showing up because of a time zone issue, you can try one of the following:\n\n1. Change the date of the commit. You can [adjust the time](https://codewithhugo.com/change-the-date-of-a-git-commit/) of a past commit to make it in the middle of the day.\n2. Create a new commit in a repository with the date set to the date that is missing from your streak stats:\n\n```bash\ngit commit --date=\"2022-08-02 12:00\" -m \"Test commit\" --allow-empty\ngit push\n```\n\n## What is considered a \"contribution\"?\n\nContributions include commits, pull requests, and issues that you create in standalone repositories ([Learn more about what is considered a contribution](https://docs.github.com/articles/why-are-my-contributions-not-showing-up-on-my-profile)).\n\nThe longest streak is the highest number of consecutive days on which you have made at least one contribution.\n\nThe 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.\n\n> 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))\n\n## How do I enable private contributions?\n\nTo include contributions in private repositories, turn on the setting for \"Private contributions\" from the dropdown menu above the contribution graph on your profile page.\n\n## How do I center the image on the page?\n\nTo center align images, you must use the HTML syntax and wrap it in an element with the HTML attribute `align=\"center\"`.\n\n<!-- prettier-ignore-start -->\n```html\n<p align=\"center\">\n    <a href=\"https://git.io/streak-stats\"><img src=\"https://streak-stats.demolab.com?user=DenverCoder1\"/></a>\n</p>\n```\n<!-- prettier-ignore-end -->\n\n## How do I make different images for dark mode and light mode?\n\nYou can [specify theme context](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) using the `<picture>` and `<source>` elements as shown below. The dark mode version appears in the `srcset` of the `<source>` tag and the light mode version appears in the `src` of the `<img>` tag.\n\n<!-- prettier-ignore-start -->\n```html\n<picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://streak-stats.demolab.com?user=DenverCoder1&theme=dark\" />\n    <img src=\"https://streak-stats.demolab.com?user=DenverCoder1&theme=default\" />\n</picture>\n```\n<!-- prettier-ignore-end -->\n\n## Why and how do I self-host GitHub Readme Streak Stats?\n\nSelf-hosting the code can be done online and only takes a couple minutes. The benefits include better uptime since it will use your own access token so will not run into ratelimiting issues and it allows you to customize the deployment for your own use case.\n\n### [📺 Click here for a video tutorial on how to self-host on Vercel](https://www.youtube.com/watch?v=maoXtlb8t44)\n\nSee [Deploying it on your own](https://github.com/DenverCoder1/github-readme-streak-stats?tab=readme-ov-file#-deploying-it-on-your-own) in the Readme for detailed instructions.\n"
  },
  {
    "path": "docs/themes.md",
    "content": "## Currently supported themes\n\nTo enable a theme, append `&theme=` followed by the theme name to the end of your url.\n\nYou can also try out and customize these themes on the [Demo Site](https://streak-stats.demolab.com/demo/)!\n\nNote: Theme names provided are case-insensitive and any use of underscores will be treated the same as hyphens.\n\n|             Theme             |                                                          Preview                                                           |\n| :---------------------------: | :------------------------------------------------------------------------------------------------------------------------: |\n|           `default`           |      ![image](https://user-images.githubusercontent.com/107488620/183304039-a1fcf05c-0112-493a-9188-778708dc9e8f.png)      |\n|            `dark`             |      ![image](https://user-images.githubusercontent.com/107488620/183304038-2788ab5d-4c02-45e9-a724-990f27061c54.png)      |\n|        `highcontrast`         |      ![image](https://user-images.githubusercontent.com/107488620/183304037-0e54b5e6-f39a-481d-806f-3369d257a391.png)      |\n|         `transparent`         |      ![image](https://user-images.githubusercontent.com/20955511/221571948-1b69a2cc-87af-4e96-83fa-f01278c22c33.png)       |\n|           `radical`           |      ![image](https://user-images.githubusercontent.com/20955511/183303809-eb8fea2f-d56b-4ad3-9f6d-ef55f8812ed2.png)       |\n|            `merko`            |      ![image](https://user-images.githubusercontent.com/20955511/183303806-4ce9e5bb-6bd7-4914-a4ff-47edee01bde3.png)       |\n|           `gruvbox`           |      ![image](https://user-images.githubusercontent.com/20955511/183303804-95ff960f-ad52-4026-8627-a67f1599cee3.png)       |\n|         `gruvbox-duo`         |      ![image](https://user-images.githubusercontent.com/20955511/183303801-eb1d8dea-7f89-4075-b334-542bb546dfcd.png)       |\n|         `tokyonight`          |      ![image](https://user-images.githubusercontent.com/20955511/183303799-e039b635-5424-437b-9f87-7ed9dca8aea6.png)       |\n|       `tokyonight-duo`        |      ![image](https://user-images.githubusercontent.com/20955511/183303796-03bb6eb2-667f-492b-8397-efd2ad93edeb.png)       |\n|           `onedark`           |      ![image](https://user-images.githubusercontent.com/20955511/183303794-54389af4-24f3-41e6-9d70-2e949d19227e.png)       |\n|         `onedark-duo`         |      ![image](https://user-images.githubusercontent.com/20955511/183303791-a4a6d5f0-ab3a-4f6e-b4cc-a87bb24fd135.png)       |\n|           `cobalt`            |      ![image](https://user-images.githubusercontent.com/20955511/183303787-eaa77366-6f13-4dc8-a0fa-637ac5333612.png)       |\n|          `synthwave`          |      ![image](https://user-images.githubusercontent.com/20955511/183303784-6257055f-d206-4d1a-bdb9-95e9dd7052fb.png)       |\n|           `dracula`           |      ![image](https://user-images.githubusercontent.com/20955511/183303782-2231d9eb-9b65-4cf9-9e26-f4cfb773abf6.png)       |\n|          `prussian`           |      ![image](https://user-images.githubusercontent.com/20955511/183303779-56649d30-2226-4797-b001-0ca1c3902132.png)       |\n|           `monokai`           |      ![image](https://user-images.githubusercontent.com/20955511/183303777-5f424f42-3c71-4802-946d-148dd4a0805f.png)       |\n|             `vue`             |      ![image](https://user-images.githubusercontent.com/20955511/183303773-44ea348d-973b-4d3c-967c-7152bba274d5.png)       |\n|          `vue-dark`           |      ![image](https://user-images.githubusercontent.com/20955511/183303769-0735cf9f-d44c-40ca-b2c1-2b56384670b4.png)       |\n|      `shades-of-purple`       |      ![image](https://user-images.githubusercontent.com/20955511/183303767-30426d56-e2bd-487a-98d7-7e5f5c8eb640.png)       |\n|          `nightowl`           |      ![image](https://user-images.githubusercontent.com/20955511/183303763-289d7a24-070f-4604-b729-8dd75eefe234.png)       |\n|            `buefy`            |      ![image](https://user-images.githubusercontent.com/20955511/183303761-3e0d060a-6a67-407a-9a0a-9c1e615cff87.png)       |\n|         `buefy-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/183303760-df6fcc74-884a-404b-9966-34363a7438b3.png)       |\n|         `blue-green`          |      ![image](https://user-images.githubusercontent.com/20955511/183303758-c8c90e09-db0d-4179-a91f-6463489fee7e.png)       |\n|           `algolia`           |      ![image](https://user-images.githubusercontent.com/20955511/183303756-2b0134af-ab8b-42d4-b805-4e853f929c5e.png)       |\n|        `great-gatsby`         |      ![image](https://user-images.githubusercontent.com/20955511/183303754-168e88f6-80db-443b-b91b-2086b164531b.png)       |\n|           `darcula`           |      ![image](https://user-images.githubusercontent.com/20955511/183303753-4b91b591-4502-4a39-9554-8ed2c7eb9777.png)       |\n|            `bear`             |      ![image](https://user-images.githubusercontent.com/20955511/183303752-5adcd734-3cdb-44f7-8c67-e42edde5ac9c.png)       |\n|       `solarized-dark`        |      ![image](https://user-images.githubusercontent.com/20955511/183303751-b1570958-bb9a-4829-9588-0d94c3fb5cfe.png)       |\n|       `solarized-light`       |      ![image](https://user-images.githubusercontent.com/20955511/183303750-03e52dfd-b052-4acd-aee6-78a1106c147e.png)       |\n|       `chartreuse-dark`       |      ![image](https://user-images.githubusercontent.com/20955511/183303749-1a489c0e-7a53-4fd5-90cd-b1271aca26e3.png)       |\n|            `nord`             |      ![image](https://user-images.githubusercontent.com/20955511/183303748-556b28e8-2f87-4657-b164-899f3216ef51.png)       |\n|           `gotham`            |      ![image](https://user-images.githubusercontent.com/20955511/183303747-bf39ce32-1bdf-4712-b4fd-abd0eb54a89e.png)       |\n|     `material-palenight`      |      ![image](https://user-images.githubusercontent.com/20955511/183303746-e73933e0-03fa-480d-9469-296852be957a.png)       |\n|          `graywhite`          |      ![image](https://user-images.githubusercontent.com/20955511/183303745-185ba0c3-a840-4a4e-95e3-03325c3b3e4e.png)       |\n|    `vision-friendly-dark`     |      ![image](https://user-images.githubusercontent.com/20955511/183303743-0c134e67-aa99-43cb-9a56-3a8b6c9fe44a.png)       |\n|         `ayu-mirage`          |      ![image](https://user-images.githubusercontent.com/20955511/183303742-31e46a33-fb80-4cf4-a966-d751d98a9c93.png)       |\n|       `midnight-purple`       |      ![image](https://user-images.githubusercontent.com/20955511/183303740-641a4a18-da69-46a8-b218-f1a6dc04fcdf.png)       |\n|            `calm`             |      ![image](https://user-images.githubusercontent.com/20955511/183303737-c00375f6-e2bc-4cf5-99c2-1544366fd260.png)       |\n|         `flag-india`          |      ![image](https://user-images.githubusercontent.com/20955511/183303735-66e35638-0fa3-40f4-b9aa-9b6c284eac8f.png)       |\n|            `omni`             |      ![image](https://user-images.githubusercontent.com/20955511/183303734-67e9f9d1-82e5-4518-8105-9105c8a13e6b.png)       |\n|            `react`            |      ![image](https://user-images.githubusercontent.com/20955511/183303733-0d994b10-1fb3-497a-8c8c-7d901dda03ed.png)       |\n|            `jolly`            |      ![image](https://user-images.githubusercontent.com/20955511/183303732-2e877a4e-f609-452d-b091-d5fb48482def.png)       |\n|         `maroongold`          |      ![image](https://user-images.githubusercontent.com/20955511/183303731-08ca9109-551d-4052-a17f-630cbb0cf323.png)       |\n|            `yeblu`            |      ![image](https://user-images.githubusercontent.com/20955511/183303730-5ffad264-362d-4ee6-82b2-15b8a8669462.png)       |\n|          `blueberry`          |      ![image](https://user-images.githubusercontent.com/20955511/183303729-f3c89ba7-efef-437e-9a05-fa5feebb9d72.png)       |\n|        `blueberry-duo`        |      ![image](https://user-images.githubusercontent.com/20955511/183303728-4d209b8c-536f-4921-aa43-6371f1e313fe.png)       |\n|         `slateorange`         |      ![image](https://user-images.githubusercontent.com/20955511/183303727-7ffec3ef-1303-4096-bd0f-f8fc1e4949e6.png)       |\n|          `kacho-ga`           |      ![image](https://user-images.githubusercontent.com/20955511/183303726-9adaaf73-2ea8-4b78-a3f4-7382ce299511.png)       |\n|       `ads-juicy-fresh`       |      ![image](https://user-images.githubusercontent.com/20955511/183303725-25851d72-963a-4532-a5ca-1eaae6c4c224.png)       |\n|          `black-ice`          |      ![image](https://user-images.githubusercontent.com/20955511/183303724-de45e18a-d4f8-48ae-88c1-d54a35d2ecea.png)       |\n|         `soft-green`          |      ![image](https://user-images.githubusercontent.com/20955511/183303722-3ae70df8-87ff-4b3b-a941-f84cef5dddf4.png)       |\n|            `blood`            |      ![image](https://user-images.githubusercontent.com/20955511/183303721-a22ea310-ebab-4ef5-bab9-2f1d7e7c566d.png)       |\n|         `blood-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/183303720-487819af-3c20-4854-8ae1-85d70115cf80.png)       |\n|          `green-nur`          |      ![image](https://user-images.githubusercontent.com/20955511/183303719-dc5ad223-cdd6-4830-9ffb-0ae965ec0159.png)       |\n|          `neon-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/183303718-8b043f5f-8d87-4370-ac42-38032e230d6e.png)       |\n|       `neon-palenight`        |      ![image](https://user-images.githubusercontent.com/20955511/183303716-bf924275-320f-44b6-8ad7-6a5f786ee9e6.png)       |\n|         `dark-smoky`          |      ![image](https://user-images.githubusercontent.com/20955511/183303715-baad8600-943a-4ad6-85d9-f7c2a46eab41.png)       |\n|      `monokai-metallian`      |      ![image](https://user-images.githubusercontent.com/20955511/183303713-2bf8ee11-a251-4d39-8aa5-ed1fd4c545ce.png)       |\n|         `city-lights`         |      ![image](https://user-images.githubusercontent.com/20955511/183303712-c9aa7429-eece-4d03-8c10-fbf28c77d495.png)       |\n|            `blux`             |      ![image](https://user-images.githubusercontent.com/20955511/183303711-ed60bb0e-9392-468b-a344-22debb20613a.png)       |\n|            `earth`            |      ![image](https://user-images.githubusercontent.com/20955511/183303710-b3c336ad-df6d-4529-aa95-6808bfe907dc.png)       |\n|          `deepblue`           |      ![image](https://user-images.githubusercontent.com/20955511/183303709-823b626b-d9c6-4e12-a146-e641a0345a2f.png)       |\n|         `holi-theme`          |      ![image](https://user-images.githubusercontent.com/20955511/183303708-83f5f757-5692-4e24-8e66-daaa8bca6b5b.png)       |\n|          `ayu-light`          |      ![image](https://user-images.githubusercontent.com/20955511/183303707-fb381b09-9963-48c8-90b9-f6b5bc67c85a.png)       |\n|         `javascript`          |      ![image](https://user-images.githubusercontent.com/20955511/183303706-4b4e34ef-6d43-4255-9a58-1d35c3127ff7.png)       |\n|       `javascript-dark`       |      ![image](https://user-images.githubusercontent.com/20955511/183303704-65313140-d66a-4f9b-9ce6-da176ecd6ec7.png)       |\n|       `noctis-minimus`        |      ![image](https://user-images.githubusercontent.com/20955511/183303703-3f774a1e-573c-48a3-a7cd-1f226784d74f.png)       |\n|         `github-dark`         |      ![image](https://user-images.githubusercontent.com/20955511/183303702-1bd5adbb-7277-4610-ad59-e5bdf20dd1de.png)       |\n|      `github-dark-blue`       |      ![image](https://user-images.githubusercontent.com/20955511/183303701-34bf6b33-812d-4afd-9c1f-70b04b2e486a.png)       |\n|        `github-light`         |      ![image](https://user-images.githubusercontent.com/20955511/183303700-7678833c-70c1-4260-8da0-5c8db7b2c38b.png)       |\n|           `elegant`           |      ![image](https://user-images.githubusercontent.com/20955511/183303699-fdd92594-83ca-486f-9ed4-a555f674d59a.png)       |\n|            `leafy`            |      ![image](https://user-images.githubusercontent.com/20955511/183303696-5129d744-af63-4874-bc99-d603ffb03b2e.png)       |\n|          `navy-gear`          |      ![image](https://user-images.githubusercontent.com/20955511/183303695-633ba0b8-11c0-49f3-988d-49390862696a.png)       |\n|           `hacker`            |      ![image](https://user-images.githubusercontent.com/20955511/183303694-e5cd3ee9-2158-41ed-8ad6-20ca7f1298cf.png)       |\n|           `garden`            |      ![image](https://user-images.githubusercontent.com/20955511/183303692-ea99a78d-be75-43fa-80ca-83f3ae454a35.png)       |\n|     `github-green-purple`     |      ![image](https://user-images.githubusercontent.com/20955511/183303691-278ec85a-197d-4a6b-abf3-593e4cc8492b.png)       |\n|           `icegray`           |      ![image](https://user-images.githubusercontent.com/20955511/183303690-7d798870-dd80-4d71-b5c2-775cc3555e14.png)       |\n|        `neon-blurange`        |      ![image](https://user-images.githubusercontent.com/20955511/183303688-7a4ceb50-84e8-47ca-8cf0-14f212227ce6.png)       |\n|         `yellowdark`          |      ![image](https://user-images.githubusercontent.com/20955511/183303687-49da2ffe-5fc9-4a0b-9ca9-c46bc394ec03.png)       |\n|          `java-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/183303686-a652b2fb-daae-4390-b245-71610aa54ef7.png)       |\n|        `android-dark`         |      ![image](https://user-images.githubusercontent.com/20955511/183303685-fed30ead-2660-48bc-b724-04fe3c394c7f.png)       |\n| `deuteranopia-friendly-theme` |      ![image](https://user-images.githubusercontent.com/107488620/183304765-9d423ff4-52ed-4a27-8a1c-2bcd290f4803.png)      |\n|        `windows-dark`         |      ![image](https://user-images.githubusercontent.com/103951737/183449796-23096f23-54b5-45af-8078-b8afd4f3baf3.png)      |\n|          `git-dark`           |      ![image](https://user-images.githubusercontent.com/103951737/183690748-060943ff-7b39-4229-b32d-806d654bd12d.png)      |\n|         `python-dark`         |      ![image](https://user-images.githubusercontent.com/103951737/183929763-ae8c93d4-0106-461c-bded-2c2adb0bd6bf.png)      |\n|             `sea`             |      ![image](https://user-images.githubusercontent.com/103951737/184303266-0e5f8a25-bfeb-4876-abf1-91a38ca87680.png)      |\n|          `sea-dark`           |      ![image](https://user-images.githubusercontent.com/103951737/184301879-953370eb-e61a-4e0f-abf4-7029c336e8f1.png)      |\n|         `violet-dark`         |      ![image](https://user-images.githubusercontent.com/103951737/184529784-05de7e57-b939-42f7-9852-345fa191c343.png)      |\n|           `horizon`           |       ![image](https://user-images.githubusercontent.com/3828247/184559656-e1f1b290-0a44-45cc-9681-010577386760.png)       |\n|          `material`           |      ![image](https://user-images.githubusercontent.com/20955511/193617994-dfab039d-b111-4a95-a00d-39517d9e40ab.png)       |\n|        `modern-lilac`         |      ![image](https://user-images.githubusercontent.com/20955511/197569406-6ff144c3-1d6e-4500-9f0b-3112a6c62584.png)       |\n|        `modern-lilac2`        |      ![image](https://user-images.githubusercontent.com/20955511/197575977-029fc730-9c7e-4556-be7c-a727a1715fa7.png)       |\n|          `halloween`          |      ![image](https://user-images.githubusercontent.com/20955511/198897937-a3c918ea-0f35-43a0-9faf-80ad8f254cdf.png)       |\n|        `violet-punch`         |      ![image](https://user-images.githubusercontent.com/20955511/199313653-d678d969-facd-4f8d-b36e-2d0ee2ce61a5.png)       |\n|      `submarine-flowers`      |      ![image](https://user-images.githubusercontent.com/20955511/201519290-14d69c90-ce17-4c63-9020-7b244ebc6fab.png)       |\n|         `rising-sun`          |      ![image](https://user-images.githubusercontent.com/20955511/221126697-2c47639d-23c5-4c23-b545-d883063deebf.png)       |\n|        `gruvbox-light`        |      ![image](https://user-images.githubusercontent.com/20955511/221585454-f9474df6-bbf4-4e3a-91e4-5e9e090e90c0.png)       |\n|           `outrun`            |      ![image](https://user-images.githubusercontent.com/20955511/221585435-d39df945-6387-4e3e-abdf-0af7dd0dabef.png)       |\n|         `ocean-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/221585476-3eb2d25c-346b-4562-808e-bf09a59b17cd.png)       |\n|     `discord-old-blurple`     |      ![image](https://user-images.githubusercontent.com/20955511/221585526-e191cb4c-9957-4ec9-85ec-8916ac691b40.png)       |\n|          `aura-dark`          |      ![image](https://user-images.githubusercontent.com/20955511/221585541-88c2a657-dbe7-47a2-b6f9-9e3cdf1fbbfe.png)       |\n|            `panda`            |      ![image](https://user-images.githubusercontent.com/20955511/221585562-1f7edc63-41c7-43c6-ac33-fd0ecb32ec5f.png)       |\n|           `cobalt2`           |      ![image](https://user-images.githubusercontent.com/20955511/221585614-256d590d-9c45-43a8-be15-48231e418bf2.png)       |\n|            `swift`            |      ![image](https://user-images.githubusercontent.com/20955511/221585640-666641b9-cc29-435c-948f-f50e58a6b330.png)       |\n|            `aura`             |      ![image](https://user-images.githubusercontent.com/20955511/221585659-f4e8a547-7f98-4438-aba9-8f13ffbcc657.png)       |\n|         `apprentice`          |      ![image](https://user-images.githubusercontent.com/20955511/221585690-155c5b01-988e-4e1c-a588-94edb0913800.png)       |\n|           `moltack`           |      ![image](https://user-images.githubusercontent.com/20955511/221585716-9e9a9bb6-17cf-458d-826c-1d9a659cdcec.png)       |\n|         `codestackr`          |      ![image](https://user-images.githubusercontent.com/20955511/221585743-c836e303-9b9a-4caf-bd12-ef83bf39bf54.png)       |\n|          `rose-pine`          |      ![image](https://user-images.githubusercontent.com/20955511/221585761-b7df70e8-b2c4-446a-a6fc-4fd13aa18117.png)       |\n|         `date-night`          |      ![image](https://user-images.githubusercontent.com/20955511/221585779-db7f394d-b3c6-49e4-ad75-bbba97530765.png)       |\n|        `one-dark-pro`         |      ![image](https://user-images.githubusercontent.com/20955511/221585805-1d10928a-286c-4945-95ed-a7317e56692f.png)       |\n|            `rose`             |      ![image](https://user-images.githubusercontent.com/20955511/221585827-e566b73a-e0c0-4711-b48c-667e6500d44e.png)       |\n|            `neon`             |      ![image](https://user-images.githubusercontent.com/20955511/225303106-8c901c48-732e-49ae-a2e6-8733254536eb.png)       |\n|       `sunset-gradient`       |      ![image](https://user-images.githubusercontent.com/20955511/233865257-3ed2bd35-458b-46bc-a189-57b0c8a2a473.png)       |\n|       `ocean-gradient`        |      ![image](https://user-images.githubusercontent.com/20955511/233865264-3bb6c04d-05d2-47b1-857c-3f9a1277651f.png)       |\n|      `ambient-gradient`       |      ![image](https://user-images.githubusercontent.com/20955511/233865269-81583e73-c9b6-4e4b-9475-bc130de1bfdd.png)       |\n|      `catppuccin-latte`       |      ![image](https://user-images.githubusercontent.com/85760664/248204601-358a8a31-4ffc-4535-a617-840926ecd4f0.png)       |\n|      `catppuccin-frappe`      |      ![image](https://user-images.githubusercontent.com/85760664/248204858-daa7bd60-1e83-4b4e-8afc-65644055235e.png)       |\n|    `catppuccin-macchiato`     |      ![image](https://user-images.githubusercontent.com/85760664/248205012-15d74ba2-746a-4efd-b2f5-bc2db87b7c10.png)       |\n|      `catppuccin-mocha`       |      ![image](https://user-images.githubusercontent.com/85760664/248204228-9f965d12-2013-48c9-b3a8-e9717b1c4e43.png)       |\n|         `burnt-neon`          |      ![image](https://user-images.githubusercontent.com/112064697/250343082-de641726-1200-4264-885a-154d539cfc3f.png)      |\n|           `humoris`           |      ![image](https://user-images.githubusercontent.com/20955511/263020536-793bedbd-cca6-47e5-92dc-c7b38ab05bce.png)       |\n|         `shadow-red`          |      ![image](https://user-images.githubusercontent.com/86386385/263407052-345edfdf-b6ee-4b53-a4c4-7dcb4948f6dc.png)       |\n|        `shadow-green`         |      ![image](https://user-images.githubusercontent.com/86386385/263407047-d769c2cf-e435-4d46-9a34-04c16f61d200.png)       |\n|         `shadow-blue`         |      ![image](https://user-images.githubusercontent.com/86386385/263407038-bdcd2ed9-4d2c-4a46-b8df-1b989ee517f5.png)       |\n|        `shadow-orange`        |      ![image](https://user-images.githubusercontent.com/86386385/263406777-07fd919b-7b4f-4fa9-ac47-3ebd0602a80b.png)       |\n|        `shadow-purple`        |      ![image](https://user-images.githubusercontent.com/86386385/263406551-46e14eac-fdbc-4b90-9df8-85c0bd1eeb41.png)       |\n|        `shadow-brown`         |      ![image](https://user-images.githubusercontent.com/86386385/263406156-5e17541d-4dcf-4315-b68d-d36c95d53767.png)       |\n|     `github-dark-dimmed`      | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/b29e3fe2-86ca-4bf5-81ce-9f6187b02c99)  |\n|          `blue-navy`          | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/29a78acd-56e8-465d-aff0-f984ecc14423)  |\n|          `calm-pink`          | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/7a789c2c-33d7-41a9-8a6e-034bbfe3e915)  |\n|       `whatsapp-light`        |      ![image](https://user-images.githubusercontent.com/86386385/266839259-1fe6a2b7-d2f2-46b0-b94d-397ff3f2a95a.png)       |\n|        `whatsapp-dark`        |      ![image](https://user-images.githubusercontent.com/86386385/266839261-d9a4a98c-ef9f-45ab-a3d6-1dca785225c3.png)       |\n|          `carbonfox`          | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/a26f8086-91de-49d7-83ca-8453cd031e72)  |\n|           `dawnfox`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/feff8dd8-d7c0-4d1d-9a84-129f1333a9e7)  |\n|           `dayfox`            | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/74111bcb-9825-4d26-a2c8-abec3618274f)  |\n|           `duskfox`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/8dfd700c-e391-4ba0-a434-db4d4455000d)  |\n|          `nightfox`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/00ab9a73-67d6-430f-8b22-da49a3e49091)  |\n|           `nordfox`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/6feef268-ed8f-4d60-bbd8-f7a0a7a58ce8)  |\n|           `terafox`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/ef943ced-365f-4ce5-965a-a9499ce1d8e1)  |\n|           `iceberg`           | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/912d8f6a-ba21-4668-9109-300c67a1f1c2)  |\n|       `whatsapp-light2`       | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/45d22825-e71b-42c7-aabf-14f50d47beef)  |\n|       `whatsapp-dark2`        | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86386385/4b41e537-368f-4f67-a1e6-81ca757ce5f7)  |\n|       `travelers-theme`       | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/20955511/45b0bb8c-fb88-4f2e-ad97-665db6bce4a7)  |\n|        `youtube-dark`         | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/62086478/6f774511-2477-46d2-b7bd-de3a57a3ca78)  |\n|         `meta-light`          | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/105522342/c9429386-0b15-4efc-9bf0-c67f4aec05d4) |\n|          `meta-dark`          | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/105522342/62119e5a-29fc-4285-ac5d-4125c49dff8c) |\n|       `dark-minimalist`       | ![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/77511070/11ba7899-1ad3-4c4b-880b-6f9e7c285f1b)  |\n|          `telegram`           |                 ![image](https://github.com/user-attachments/assets/59a5d9d5-8a2a-4916-aa46-a0a49a6f0372)                  |\n|            `taiga`            |                 ![image](https://github.com/user-attachments/assets/be4e961d-a13e-401a-90f8-f2b062a8c0f9)                  |\n|      `telegram-gradient`      |                 ![image](https://github.com/user-attachments/assets/985c3e04-a5dd-4cba-a66e-d43ad9668af0)                  |\n|          `microsoft`          |                 ![image](https://github.com/user-attachments/assets/4c2cce9d-90b5-4e38-8422-656c5a78b4d9)                  |\n|       `microsoft-dark`        |                 ![image](https://github.com/user-attachments/assets/a5918d7d-f568-4012-b06f-d9cfacaece04)                  |\n|       `hacker-inverted`       |                 ![image](https://github.com/user-attachments/assets/b64c136a-827b-4177-98f9-28db59bba0ef)                  |\n|      `rust-ferris-light`      |                 ![image](https://github.com/user-attachments/assets/2e1d175f-c39d-4e56-be41-d9c277f1e83a)                  |\n|      `rust-ferris-dark`       |                 ![image](https://github.com/user-attachments/assets/05e3f9ac-708d-415d-990f-ede3d0a84bab)                  |\n|      `cyber-streakglow`       |                 ![image](https://github.com/user-attachments/assets/8c6108e1-f3a1-4653-9f68-08ed6dcfc498)                  |\n|           `vitesse`           |                 ![image](https://github.com/user-attachments/assets/baa2fa20-36ea-4158-befc-79c21f102f87)                  |\n|         `nord-aurora`         |                 ![image](https://github.com/user-attachments/assets/d61bf5c3-66f2-4c02-bd9d-30bf1be47c97)                  |\n|          `dark-aura`          |                 ![Image](https://github.com/user-attachments/assets/14889d0e-26db-4fa6-8026-6312c9b4636e)                  |\n|       `everforest-dark`       |                 ![image](https://github.com/user-attachments/assets/45a4e0a0-d330-4233-9d76-89003e59bb31)                  |\n|      `everforest-light`       |                 ![image](https://github.com/user-attachments/assets/592466c0-5a67-48cc-adf0-f8a21ca891b6)                  |\n|        `oceanic-next`         |                 ![image](https://github.com/user-attachments/assets/e0182770-a511-42b6-a40b-644317268a0f)                  |\n|       `kanagawa-paper`        |                 ![image](https://github.com/user-attachments/assets/541a521d-a6a8-4b55-ab79-7b1a9bdb092c)                  |\n|          `sakura-x`           |                 ![image](https://github.com/user-attachments/assets/65360cfa-9d5e-42f2-b3c9-cc2815623413)                  |\n\n### Can't find the theme you like?\n\nYou can now customize your stats card with the interactive [Demo Site](https://streak-stats.demolab.com/demo/) or by customizing the [url parameters](/README.md#-options).\n\nIf you would like to share your theme with others, feel free to open an issue/pull request!\n\nNote: When submitting a new theme, make sure the name is all lowercase. Hyphens are allowed between words, but there should be no underscores. On the demo site, you can export a list of colors from the advanced section by clicking \"Export to PHP\".\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"engines\": {\n    \"node\": \"22.x\"\n  },\n  \"devDependencies\": {\n    \"@prettier/plugin-php\": \"^0.24.0\",\n    \"prettier\": \"^3.6.2\"\n  }\n}\n"
  },
  {
    "path": "scripts/translation-progress.php",
    "content": "<?php\n\n$TRANSLATIONS = include __DIR__ . \"/../src/translations.php\";\n\n/**\n * Get the percentage of translated phrases for each locale\n *\n * @param array $translations The translations array\n * @return array The percentage of translated phrases for each locale\n */\nfunction getProgress(array $translations): array\n{\n    $phrases_to_translate = [\n        \"Total Contributions\",\n        \"Current Streak\",\n        \"Longest Streak\",\n        \"Week Streak\",\n        \"Longest Week Streak\",\n        \"Present\",\n        \"Excluding {days}\",\n    ];\n\n    $translations_file = file(__DIR__ . \"/../src/translations.php\");\n    $progress = [];\n    foreach ($translations as $locale => $phrases) {\n        // skip aliases\n        if (is_string($phrases)) {\n            continue;\n        }\n        $translated = 0;\n        foreach ($phrases_to_translate as $phrase) {\n            if (isset($phrases[$phrase])) {\n                $translated++;\n            }\n        }\n        $percentage = round(($translated / count($phrases_to_translate)) * 100);\n        $locale_name = Locale::getDisplayName($locale, $locale);\n        $line_number = getLineNumber($translations_file, $locale);\n        $progress[$locale] = [\n            \"locale\" => $locale,\n            \"locale_name\" => $locale_name,\n            \"percentage\" => $percentage,\n            \"line_number\" => $line_number,\n        ];\n    }\n    // sort by percentage\n    uasort($progress, function ($a, $b) {\n        return $b[\"percentage\"] <=> $a[\"percentage\"];\n    });\n    return $progress;\n}\n\n/**\n * Get the line number of the locale in the translations file\n *\n * @param array $translations_file The translations file\n * @param string $locale The locale\n * @return int The line number of the locale in the translations file\n */\nfunction getLineNumber(array $translations_file, string $locale): int\n{\n    return key(preg_grep(\"/^\\\\s*\\\"$locale\\\"\\\\s*=>\\\\s*\\\\[/\", $translations_file)) + 1;\n}\n\n/**\n * Convert progress to labeled badges\n *\n * @param array $progress The progress array\n * @return string The markdown for the image badges\n */\nfunction progressToBadges(array $progress): string\n{\n    $per_row = 5;\n    $table = \"<table><tbody>\";\n    $i = 0;\n    foreach (array_values($progress) as $data) {\n        if ($i % $per_row === 0) {\n            $table .= \"<tr>\";\n        }\n        $line_url = \"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/src/translations.php#L{$data[\"line_number\"]}\";\n        $table .= \"<td><a href=\\\"{$line_url}\\\"><code>{$data[\"locale\"]}</code></a> - {$data[\"locale_name\"]}<br /><a href=\\\"{$line_url}\\\"><img src=\\\"https://progress-bar.xyz/{$data[\"percentage\"]}\\\" alt=\\\"{$data[\"locale_name\"]} {$data[\"percentage\"]}%\\\"></a></td>\";\n        $i++;\n        if ($i % $per_row === 0) {\n            $table .= \"</tr>\";\n        }\n    }\n    if ($i % $per_row !== 0) {\n        while ($i % $per_row !== 0) {\n            $table .= \"<td></td>\";\n            $i++;\n        }\n        $table .= \"</tr>\";\n    }\n    $table .= \"</tbody></table>\\n\";\n    return $table;\n}\n\n/**\n * Update readme by replacing the content between the start and end markers\n *\n * @param string $path The path to the readme file\n * @param string $start The start marker\n * @param string $end The end marker\n * @param string $content The content to replace the content between the start and end markers\n * @return int|false The number of bytes that were written to the file, or false on failure\n */\nfunction updateReadme(string $path, string $start, string $end, string $content): int|false\n{\n    $readme = file_get_contents($path);\n    if (strpos($readme, $start) === false || strpos($readme, $end) === false) {\n        throw new Exception(\"Start or end marker not found in readme\");\n    }\n    $start_pos = strpos($readme, $start) + strlen($start);\n    $end_pos = strpos($readme, $end);\n    $length = $end_pos - $start_pos;\n    $readme = substr_replace($readme, $content, $start_pos, $length);\n    return file_put_contents($path, $readme);\n}\n\n$progress = getProgress($GLOBALS[\"TRANSLATIONS\"]);\n$badges = \"\\n\" . progressToBadges($progress);\n$update = updateReadme(\n    __DIR__ . \"/../README.md\",\n    \"<!-- TRANSLATION_PROGRESS_START -->\",\n    \"<!-- TRANSLATION_PROGRESS_END -->\",\n    $badges,\n);\nexit($update === false ? 1 : 0);\n"
  },
  {
    "path": "src/cache.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * Simple file-based cache for GitHub contribution stats\n *\n * Caches stats for 24 hours to avoid repeated API calls\n */\n\n// Default cache duration: 24 hours (in seconds)\ndefine(\"CACHE_DURATION\", 24 * 60 * 60);\ndefine(\"CACHE_DIR\", __DIR__ . \"/../cache\");\n\n/**\n * Generate a cache key for a user's request\n *\n * Uses structured JSON format to prevent hash collisions between different\n * user/options combinations that could produce the same concatenated string.\n *\n * @param string $user GitHub username\n * @param array $options Additional options that affect the stats (mode, exclude_days, starting_year)\n * @return string Cache key (filename-safe)\n */\nfunction getCacheKey(string $user, array $options = []): string\n{\n    ksort($options);\n    try {\n        $keyData = json_encode([\"user\" => $user, \"options\" => $options], JSON_THROW_ON_ERROR);\n    } catch (JsonException $e) {\n        // Fallback to simple concatenation if JSON encoding fails\n        error_log(\"Cache key JSON encoding failed: \" . $e->getMessage());\n        $keyData = $user . serialize($options);\n    }\n    return hash(\"sha256\", $keyData);\n}\n\n/**\n * Get the cache file path for a given key\n *\n * @param string $key Cache key\n * @return string Full path to cache file\n */\nfunction getCacheFilePath(string $key): string\n{\n    return CACHE_DIR . \"/\" . $key . \".json\";\n}\n\n/**\n * Ensure the cache directory exists\n *\n * @return bool True if directory exists or was created\n */\nfunction ensureCacheDir(): bool\n{\n    if (!is_dir(CACHE_DIR)) {\n        return mkdir(CACHE_DIR, 0755, true);\n    }\n    return true;\n}\n\n/**\n * Get cached stats if available and not expired\n *\n * @param string $user GitHub username\n * @param array $options Additional options\n * @param int $maxAge Maximum age in seconds (default: 24 hours)\n * @return array|null Cached stats array or null if not cached/expired\n */\nfunction getCachedStats(string $user, array $options = [], int $maxAge = CACHE_DURATION): ?array\n{\n    $key = getCacheKey($user, $options);\n    $filePath = getCacheFilePath($key);\n\n    if (!file_exists($filePath)) {\n        return null;\n    }\n\n    $mtime = filemtime($filePath);\n    if ($mtime === false) {\n        return null;\n    }\n\n    $fileAge = time() - $mtime;\n    if ($fileAge > $maxAge) {\n        unlink($filePath);\n        return null;\n    }\n\n    $handle = fopen($filePath, \"r\");\n    if ($handle === false) {\n        return null;\n    }\n\n    if (!flock($handle, LOCK_SH)) {\n        fclose($handle);\n        return null;\n    }\n\n    $contents = stream_get_contents($handle);\n    flock($handle, LOCK_UN);\n    fclose($handle);\n\n    if ($contents === false || $contents === \"\") {\n        return null;\n    }\n\n    $data = json_decode($contents, true);\n    if (!is_array($data)) {\n        return null;\n    }\n\n    return $data;\n}\n\n/**\n * Save stats to cache\n *\n * @param string $user GitHub username\n * @param array $options Additional options\n * @param array $stats Stats array to cache\n * @return bool True if successfully cached\n */\nfunction setCachedStats(string $user, array $options, array $stats): bool\n{\n    if (!ensureCacheDir()) {\n        error_log(\"Failed to create cache directory: \" . CACHE_DIR);\n        return false;\n    }\n\n    $key = getCacheKey($user, $options);\n    $filePath = getCacheFilePath($key);\n\n    $data = json_encode($stats);\n    if ($data === false) {\n        error_log(\"Failed to encode stats to JSON for user: \" . $user);\n        return false;\n    }\n\n    $result = file_put_contents($filePath, $data, LOCK_EX);\n    if ($result === false) {\n        error_log(\"Failed to write cache file: \" . $filePath);\n        return false;\n    }\n\n    return true;\n}\n\n/**\n * Clear all expired cache files\n *\n * @param int $maxAge Maximum age in seconds\n * @return int Number of files deleted\n */\nfunction clearExpiredCache(int $maxAge = CACHE_DURATION): int\n{\n    if (!is_dir(CACHE_DIR)) {\n        return 0;\n    }\n\n    $deleted = 0;\n    $files = glob(CACHE_DIR . \"/*.json\");\n\n    if ($files === false) {\n        return 0;\n    }\n\n    foreach ($files as $file) {\n        $mtime = filemtime($file);\n        if ($mtime === false) {\n            continue;\n        }\n        $fileAge = time() - $mtime;\n        if ($fileAge > $maxAge) {\n            if (unlink($file)) {\n                $deleted++;\n            }\n        }\n    }\n\n    return $deleted;\n}\n\n/**\n * Clear cache for a specific user\n *\n * Note: This function only clears the cache for the user with empty/default options.\n * Cache entries with non-empty options (starting_year, mode, exclude_days) will NOT\n * be cleared. This is a limitation of the hash-based cache key system - we cannot\n * enumerate all possible option combinations without storing additional metadata.\n *\n * @param string $user GitHub username\n * @return bool True if cache was cleared (or didn't exist)\n */\nfunction clearUserCache(string $user): bool\n{\n    if (!is_dir(CACHE_DIR)) {\n        return true;\n    }\n\n    $key = getCacheKey($user, []);\n    $filePath = getCacheFilePath($key);\n\n    if (file_exists($filePath)) {\n        return unlink($filePath);\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "src/card.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * Convert date from Y-M-D to more human-readable format\n *\n * @param string $dateString String in Y-M-D format\n * @param string|null $format Date format to use, or null to use locale default\n * @param string $locale Locale code\n * @return string Formatted Date string\n */\nfunction formatDate(string $dateString, string|null $format, string $locale): string\n{\n    $date = new DateTime($dateString);\n    $formatted = \"\";\n    $patternGenerator = new IntlDatePatternGenerator($locale);\n    // if current year, display only month and day\n    if (date_format($date, \"Y\") == date(\"Y\")) {\n        if ($format) {\n            // remove brackets and all text within them\n            $formatted = date_format($date, preg_replace(\"/\\[.*?\\]/\", \"\", $format));\n        } else {\n            // format without year using locale\n            $pattern = $patternGenerator->getBestPattern(\"MMM d\");\n            $dateFormatter = new IntlDateFormatter(\n                $locale,\n                IntlDateFormatter::MEDIUM,\n                IntlDateFormatter::NONE,\n                pattern: $pattern,\n            );\n            $formatted = $dateFormatter->format($date);\n        }\n    }\n    // otherwise, display month, day, and year\n    else {\n        if ($format) {\n            // remove brackets, but leave text within them\n            $formatted = date_format($date, str_replace([\"[\", \"]\"], \"\", $format));\n        } else {\n            // format with year using locale\n            $pattern = $patternGenerator->getBestPattern(\"yyyy MMM d\");\n            $dateFormatter = new IntlDateFormatter(\n                $locale,\n                IntlDateFormatter::MEDIUM,\n                IntlDateFormatter::NONE,\n                pattern: $pattern,\n            );\n            $formatted = $dateFormatter->format($date);\n        }\n    }\n    // sanitize and return formatted date\n    return htmlspecialchars($formatted);\n}\n\n/**\n * Translate days of the week\n *\n * Takes a list of days (eg. [\"Sun\", \"Mon\", \"Sat\"]) and returns the short abbreviation of the days of the week in another locale\n * e.g. [\"Sun\", \"Mon\", \"Sat\"] -> [\"dim\", \"lun\", \"sam\"]\n *\n * @param array<string> $days List of days to translate\n * @param string $locale Locale code\n *\n * @return array<string> Translated days\n */\nfunction translateDays(array $days, string $locale): array\n{\n    if ($locale === \"en\") {\n        return $days;\n    }\n    $patternGenerator = new IntlDatePatternGenerator($locale);\n    $pattern = $patternGenerator->getBestPattern(\"EEE\");\n    $dateFormatter = new IntlDateFormatter(\n        $locale,\n        IntlDateFormatter::NONE,\n        IntlDateFormatter::NONE,\n        pattern: $pattern,\n    );\n    $translatedDays = [];\n    foreach ($days as $day) {\n        $translatedDays[] = $dateFormatter->format(new DateTime($day));\n    }\n    return $translatedDays;\n}\n\n/**\n * Get the excluding days text\n *\n * @param array<string> $excludedDays List of excluded days\n * @param array<string,string> $localeTranslations Translations for the locale\n * @param string $localeCode Locale code\n * @return string Excluding days text\n */\nfunction getExcludingDaysText($excludedDays, $localeTranslations, $localeCode)\n{\n    $separator = $localeTranslations[\"comma_separator\"] ?? \", \";\n    $daysCommaSeparated = implode($separator, translateDays($excludedDays, $localeCode));\n    return str_replace(\"{days}\", $daysCommaSeparated, $localeTranslations[\"Excluding {days}\"]);\n}\n\n/**\n * Normalize a theme name\n *\n * @param string $theme Theme name\n * @return string Normalized theme name\n */\nfunction normalizeThemeName(string $theme): string\n{\n    return strtolower(str_replace(\"_\", \"-\", $theme));\n}\n\n/**\n * Check theme and color customization parameters to generate a theme mapping\n *\n * @param array<string,string> $params Request parameters\n * @return array<string,string> The chosen theme or default\n */\nfunction getRequestedTheme(array $params): array\n{\n    /**\n     * @var array<string,array<string,string>> $THEMES\n     * List of theme names mapped to labeled colors\n     */\n    $THEMES = include \"themes.php\";\n\n    /**\n     * @var array<string> $CSS_COLORS\n     * List of valid CSS colors\n     */\n    $CSS_COLORS = include \"colors.php\";\n\n    // normalize theme name\n    $selectedTheme = normalizeThemeName($params[\"theme\"] ?? \"default\");\n\n    // get theme colors, or default colors if theme not found\n    $theme = $THEMES[$selectedTheme] ?? $THEMES[\"default\"];\n\n    // personal theme customizations\n    $properties = array_keys($theme);\n    foreach ($properties as $prop) {\n        // check if each property was passed as a parameter\n        if (isset($params[$prop])) {\n            // ignore case\n            $param = strtolower($params[$prop]);\n            // check if color is valid hex color (3, 4, 6, or 8 hex digits)\n            if (preg_match(\"/^([a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/\", $param)) {\n                // set property\n                $theme[$prop] = \"#\" . $param;\n            }\n            // check if color is valid css color\n            elseif (in_array($param, $CSS_COLORS)) {\n                // set property\n                $theme[$prop] = $param;\n            }\n            // if the property is background gradient is allowed (angle,start_color,...,end_color)\n            elseif ($prop == \"background\" && preg_match(\"/^-?[0-9]+,[a-f0-9]{3,8}(,[a-f0-9]{3,8})+$/\", $param)) {\n                // set property\n                $theme[$prop] = $param;\n            }\n        }\n    }\n\n    // hide borders\n    if (isset($params[\"hide_border\"]) && $params[\"hide_border\"] == \"true\") {\n        $theme[\"border\"] = \"#0000\"; // transparent\n    }\n\n    // set background\n    $gradient = \"\";\n    $backgroundParts = explode(\",\", $theme[\"background\"] ?? \"\");\n    if (count($backgroundParts) >= 3) {\n        $theme[\"background\"] = \"url(#gradient)\";\n        $gradient = \"<linearGradient id='gradient' gradientTransform='rotate({$backgroundParts[0]})' gradientUnits='userSpaceOnUse'>\";\n        $backgroundColors = array_slice($backgroundParts, 1);\n        $colorCount = count($backgroundColors);\n        for ($index = 0; $index < $colorCount; $index++) {\n            $offset = ($index * 100) / ($colorCount - 1);\n            $gradient .= \"<stop offset='{$offset}%' stop-color='#{$backgroundColors[$index]}' />\";\n        }\n        $gradient .= \"</linearGradient>\";\n    }\n    $theme[\"backgroundGradient\"] = $gradient;\n\n    return $theme;\n}\n\n/**\n * Wraps a string to a given number of characters\n *\n * Similar to `wordwrap()`, but uses regex and does not break with certain non-ascii characters\n *\n * @param string $string The input string\n * @param int $width The number of characters at which the string will be wrapped\n * @param string $break The line is broken using the optional `break` parameter\n * @param bool $cut_long_words If the `cut_long_words` parameter is set to true, the string is\n *              the string is always wrapped at or before the specified width. So if you have\n *              a word that is larger than the given width, it is broken apart.\n *              When false the function does not split the word even if the width is smaller\n *              than the word width.\n * @return string The given string wrapped at the specified length\n */\nfunction utf8WordWrap(string $string, int $width = 75, string $break = \"\\n\", bool $cut_long_words = false): string\n{\n    // match anything 1 to $width chars long followed by whitespace or EOS\n    $string = preg_replace(\"/(.{1,$width})(?:\\s|$)/uS\", \"$1$break\", $string);\n    // split words that are too long after being broken up\n    if ($cut_long_words) {\n        $string = preg_replace(\"/(\\S{\" . $width . \"})(?=\\S)/u\", \"$1$break\", $string);\n    }\n    // trim any trailing line breaks\n    return rtrim($string, $break);\n}\n\n/**\n * Get the length of a string with utf8 characters\n *\n * Similar to `strlen()`, but uses regex and does not break with certain non-ascii characters\n *\n * @param string $string The input string\n * @return int The length of the string\n */\nfunction utf8Strlen(string $string): int\n{\n    return preg_match_all(\"/./us\", $string, $matches);\n}\n\n/**\n * Split lines of text using <tspan> elements if it contains a newline or exceeds a maximum number of characters\n *\n * @param string $text Text to split\n * @param int $maxChars Maximum number of characters per line\n * @param int $line1Offset Offset for the first line\n * @return string Original text if one line, or split text with <tspan> elements\n */\nfunction splitLines(string $text, int $maxChars, int $line1Offset): string\n{\n    // if too many characters, insert \\n before a \" \" or \"-\" if possible\n    if ($maxChars > 0 && utf8Strlen($text) > $maxChars && strpos($text, \"\\n\") === false) {\n        // prefer splitting at \" - \" if possible\n        if (strpos($text, \" - \") !== false) {\n            $text = str_replace(\" - \", \"\\n- \", $text);\n        }\n        // otherwise, use word wrap to split at spaces\n        else {\n            $text = utf8WordWrap($text, $maxChars, \"\\n\", true);\n        }\n    }\n    $text = htmlspecialchars($text);\n    return preg_replace(\n        \"/^(.*)\\n(.*)/\",\n        \"<tspan x='0' dy='{$line1Offset}'>$1</tspan><tspan x='0' dy='16'>$2</tspan>\",\n        $text,\n    );\n}\n\n/**\n * Normalize a locale code\n *\n * @param string $localeCode Locale code\n * @return string Normalized locale code\n */\nfunction normalizeLocaleCode(string $localeCode): string\n{\n    preg_match(\"/^([a-z]{2,3})(?:[_-]([a-z]{4}))?(?:[_-]([0-9]{3}|[a-z]{2}))?$/i\", $localeCode, $matches);\n    if (empty($matches)) {\n        return \"en\";\n    }\n    $language = $matches[1];\n    $script = $matches[2] ?? \"\";\n    $region = $matches[3] ?? \"\";\n    // convert language to lowercase\n    $language = strtolower($language);\n    // convert script to title case\n    $script = ucfirst(strtolower($script));\n    // convert region to uppercase\n    $region = strtoupper($region);\n    // combine language, script, and region using underscores\n    return implode(\"_\", array_filter([$language, $script, $region]));\n}\n\n/**\n * Get the translations for a locale code after normalizing it\n *\n * @param string $localeCode Locale code\n * @return array Translations for the locale code\n */\nfunction getTranslations(string $localeCode): array\n{\n    // normalize locale code\n    $localeCode = normalizeLocaleCode($localeCode);\n    // get the labels from the translations file\n    $translations = include \"translations.php\";\n    // if the locale does not exist, try without the script and region\n    if (!isset($translations[$localeCode])) {\n        $localeCode = explode(\"_\", $localeCode)[0];\n    }\n    // get the translations for the locale or empty array if it does not exist\n    $localeTranslations = $translations[$localeCode] ?? [];\n    // if the locale returned is a string, it is an alias for another locale\n    if (is_string($localeTranslations)) {\n        // get the translations for the alias\n        $localeTranslations = $translations[$localeTranslations];\n    }\n    // fill in missing translations with English\n    $localeTranslations += $translations[\"en\"];\n    // return the translations\n    return $localeTranslations;\n}\n\n/**\n * Get the card width from params taking into account minimum and default values\n *\n * @param array<string,string> $params Request parameters\n * @param int $numColumns Number of columns in the card\n * @return int Card width\n */\nfunction getCardWidth(array $params, int $numColumns = 3): int\n{\n    $defaultWidth = 495;\n    $minimumWidth = 100 * $numColumns;\n    return max($minimumWidth, intval($params[\"card_width\"] ?? $defaultWidth));\n}\n\n/**\n * Get the card height from params taking into account minimum and default values\n *\n * @param array<string,string> $params Request parameters\n * @return int Card width\n */\nfunction getCardHeight(array $params): int\n{\n    $defaultHeight = 195;\n    $minimumHeight = 170;\n    return max($minimumHeight, intval($params[\"card_height\"] ?? $defaultHeight));\n}\n\n/**\n * Format number using locale and short number if requested\n *\n * @param float $num The number to format\n * @param string $localeCode Locale code\n * @param bool $useShortNumbers Whether to use short numbers\n * @return string The formatted number\n */\nfunction formatNumber(float $num, string $localeCode, bool $useShortNumbers): string\n{\n    $numFormatter = new NumberFormatter($localeCode, NumberFormatter::DECIMAL);\n    $suffix = \"\";\n    if ($useShortNumbers) {\n        $units = [\"\", \"K\", \"M\", \"B\", \"T\"];\n        for ($i = 0; $num >= 1000; $i++) {\n            $num /= 1000;\n        }\n        $suffix = $units[$i];\n        $num = round($num, 1);\n    }\n    return $numFormatter->format($num) . $suffix;\n}\n\n/**\n * Generate SVG output for a stats array\n *\n * @param array<string,mixed> $stats Streak stats\n * @param array<string,string>|NULL $params Request parameters\n * @return string The generated SVG Streak Stats card\n *\n * @throws InvalidArgumentException If a locale does not exist\n */\nfunction generateCard(array $stats, array $params = null): string\n{\n    $params = $params ?? $_REQUEST;\n\n    // get requested theme\n    $theme = getRequestedTheme($params);\n\n    // get requested locale, default to English\n    $localeCode = $params[\"locale\"] ?? \"en\";\n    $localeTranslations = getTranslations($localeCode);\n\n    // whether the locale is right-to-left\n    $direction = $localeTranslations[\"rtl\"] ?? false ? \"rtl\" : \"ltr\";\n\n    // get date format\n    // locale date formatter (used only if date_format is not specified)\n    $dateFormat = $params[\"date_format\"] ?? ($localeTranslations[\"date_format\"] ?? null);\n\n    // read border_radius parameter, default to 4.5 if not set\n    $borderRadius = $params[\"border_radius\"] ?? 4.5;\n\n    $showTotalContributions = ($params[\"hide_total_contributions\"] ?? \"\") !== \"true\";\n    $showCurrentStreak = ($params[\"hide_current_streak\"] ?? \"\") !== \"true\";\n    $showLongestStreak = ($params[\"hide_longest_streak\"] ?? \"\") !== \"true\";\n    $numColumns = intval($showTotalContributions) + intval($showCurrentStreak) + intval($showLongestStreak);\n\n    $cardWidth = getCardWidth($params, $numColumns);\n    $rectWidth = $cardWidth - 1;\n    $columnWidth = $numColumns > 0 ? $cardWidth / $numColumns : 0;\n\n    $cardHeight = getCardHeight($params);\n    $rectHeight = $cardHeight - 1;\n    $heightOffset = ($cardHeight - 195) / 2;\n\n    // X offsets for the bars between columns\n    $barOffsets = [-999, -999];\n    for ($i = 0; $i < $numColumns - 1; $i++) {\n        $barOffsets[$i] = $columnWidth * ($i + 1);\n    }\n    // offsets for the text in each column\n    $columnOffsets = [];\n    for ($i = 0; $i < $numColumns; $i++) {\n        $columnOffsets[] = $columnWidth / 2 + $columnWidth * $i;\n    }\n    // reverse the column offsets if the locale is right-to-left\n    if ($direction === \"rtl\") {\n        $columnOffsets = array_reverse($columnOffsets);\n    }\n\n    $nextColumnIndex = 0;\n    $totalContributionsOffset = $showTotalContributions ? $columnOffsets[$nextColumnIndex++] : -999;\n    $currentStreakOffset = $showCurrentStreak ? $columnOffsets[$nextColumnIndex++] : -999;\n    $longestStreakOffset = $showLongestStreak ? $columnOffsets[$nextColumnIndex++] : -999;\n\n    // Y offsets for the bars\n    $barHeightOffsets = [28 + $heightOffset / 2, 170 + $heightOffset];\n    // Y offsets for the numbers and dates\n    $longestStreakHeightOffset = $totalContributionsHeightOffset = [\n        48 + $heightOffset,\n        84 + $heightOffset,\n        114 + $heightOffset,\n    ];\n    $currentStreakHeightOffset = [\n        48 + $heightOffset,\n        108 + $heightOffset,\n        145 + $heightOffset,\n        71 + $heightOffset,\n        19.5 + $heightOffset,\n    ];\n\n    $useShortNumbers = ($params[\"short_numbers\"] ?? \"\") === \"true\";\n\n    // total contributions\n    $totalContributions = formatNumber($stats[\"totalContributions\"], $localeCode, $useShortNumbers);\n    $firstContribution = formatDate($stats[\"firstContribution\"], $dateFormat, $localeCode);\n    $totalContributionsRange = $firstContribution . \" - \" . $localeTranslations[\"Present\"];\n\n    // current streak\n    $currentStreak = formatNumber($stats[\"currentStreak\"][\"length\"], $localeCode, $useShortNumbers);\n    $currentStreakStart = formatDate($stats[\"currentStreak\"][\"start\"], $dateFormat, $localeCode);\n    $currentStreakEnd = formatDate($stats[\"currentStreak\"][\"end\"], $dateFormat, $localeCode);\n    $currentStreakRange = $currentStreakStart;\n    if ($currentStreakStart != $currentStreakEnd) {\n        $currentStreakRange .= \" - \" . $currentStreakEnd;\n    }\n\n    // longest streak\n    $longestStreak = formatNumber($stats[\"longestStreak\"][\"length\"], $localeCode, $useShortNumbers);\n    $longestStreakStart = formatDate($stats[\"longestStreak\"][\"start\"], $dateFormat, $localeCode);\n    $longestStreakEnd = formatDate($stats[\"longestStreak\"][\"end\"], $dateFormat, $localeCode);\n    $longestStreakRange = $longestStreakStart;\n    if ($longestStreakStart != $longestStreakEnd) {\n        $longestStreakRange .= \" - \" . $longestStreakEnd;\n    }\n\n    // if the translations contain over max characters or a newline, split the text into two tspan elements\n    $maxCharsPerLineLabels = $numColumns > 0 ? intval(floor($cardWidth / $numColumns / 7.5)) : 0;\n    $totalContributionsText = splitLines($localeTranslations[\"Total Contributions\"], $maxCharsPerLineLabels, -9);\n    if ($stats[\"mode\"] === \"weekly\") {\n        $currentStreakText = splitLines($localeTranslations[\"Week Streak\"], $maxCharsPerLineLabels, -9);\n        $longestStreakText = splitLines($localeTranslations[\"Longest Week Streak\"], $maxCharsPerLineLabels, -9);\n    } else {\n        $currentStreakText = splitLines($localeTranslations[\"Current Streak\"], $maxCharsPerLineLabels, -9);\n        $longestStreakText = splitLines($localeTranslations[\"Longest Streak\"], $maxCharsPerLineLabels, -9);\n    }\n\n    // if the ranges contain over max characters, split the text into two tspan elements\n    $maxCharsPerLineDates = $numColumns > 0 ? intval(floor($cardWidth / $numColumns / 6)) : 0;\n    $totalContributionsRange = splitLines($totalContributionsRange, $maxCharsPerLineDates, 0);\n    $currentStreakRange = splitLines($currentStreakRange, $maxCharsPerLineDates, 0);\n    $longestStreakRange = splitLines($longestStreakRange, $maxCharsPerLineDates, 0);\n\n    // if days are excluded, add a note to the corner\n    $excludedDays = \"\";\n    if (!empty($stats[\"excludedDays\"])) {\n        $offset = $direction === \"rtl\" ? $cardWidth - 5 : 5;\n        $excludingDaysText = getExcludingDaysText($stats[\"excludedDays\"], $localeTranslations, $localeCode);\n        $excludedDays = \"<g style='isolation: isolate'>\n                <!-- Excluded Days -->\n                <g transform='translate({$offset},187)'>\n                    <text stroke-width='0' text-anchor='right' fill='{$theme[\"excludeDaysLabel\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='10px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>\n                        * {$excludingDaysText}\n                    </text>\n                </g>\n            </g>\";\n    }\n\n    return \"<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'\n                style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px' direction='{$direction}'>\n        <style>\n            @keyframes currstreak {\n                0% { font-size: 3px; opacity: 0.2; }\n                80% { font-size: 34px; opacity: 1; }\n                100% { font-size: 28px; opacity: 1; }\n            }\n            @keyframes fadein {\n                0% { opacity: 0; }\n                100% { opacity: 1; }\n            }\n        </style>\n        <defs>\n            <clipPath id='outer_rectangle'>\n                <rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>\n            </clipPath>\n            <mask id='mask_out_ring_behind_fire'>\n                <rect width='{$cardWidth}' height='{$cardHeight}' fill='white'/>\n                <ellipse id='mask-ellipse' cx='{$currentStreakOffset}' cy='32' rx='13' ry='18' fill='black'/>\n            </mask>\n            {$theme[\"backgroundGradient\"]}\n        </defs>\n        <g clip-path='url(#outer_rectangle)'>\n            <g style='isolation: isolate'>\n                <rect stroke='{$theme[\"border\"]}' fill='{$theme[\"background\"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>\n            </g>\n            <g style='isolation: isolate'>\n                <line x1='{$barOffsets[0]}' y1='{$barHeightOffsets[0]}' x2='{$barOffsets[0]}' y2='{$barHeightOffsets[1]}' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme[\"stroke\"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>\n                <line x1='{$barOffsets[1]}' y1='$barHeightOffsets[0]' x2='{$barOffsets[1]}' y2='$barHeightOffsets[1]' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme[\"stroke\"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>\n            </g>\n            <g style='isolation: isolate'>\n                <!-- Total Contributions big number -->\n                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[0]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"sideNums\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>\n                        {$totalContributions}\n                    </text>\n                </g>\n\n                <!-- Total Contributions label -->\n                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[1]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"sideLabels\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>\n                        {$totalContributionsText}\n                    </text>\n                </g>\n\n                <!-- Total Contributions range -->\n                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[2]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"dates\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>\n                        {$totalContributionsRange}\n                    </text>\n                </g>\n            </g>\n            <g style='isolation: isolate'>\n                <!-- Current Streak label -->\n                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[1]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"currStreakLabel\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>\n                        {$currentStreakText}\n                    </text>\n                </g>\n\n                <!-- Current Streak range -->\n                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[2]})'>\n                    <text x='0' y='21' stroke-width='0' text-anchor='middle' fill='{$theme[\"dates\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>\n                        {$currentStreakRange}\n                    </text>\n                </g>\n\n                <!-- Ring around number -->\n                <g mask='url(#mask_out_ring_behind_fire)'>\n                    <circle cx='{$currentStreakOffset}' cy='{$currentStreakHeightOffset[3]}' r='40' fill='none' stroke='{$theme[\"ring\"]}' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>\n                </g>\n                <!-- Fire icon -->\n                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[4]})' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>\n                    <path d='M -12 -0.5 L 15 -0.5 L 15 23.5 L -12 23.5 L -12 -0.5 Z' fill='none'/>\n                    <path d='M 1.5 0.67 C 1.5 0.67 2.24 3.32 2.24 5.47 C 2.24 7.53 0.89 9.2 -1.17 9.2 C -3.23 9.2 -4.79 7.53 -4.79 5.47 L -4.76 5.11 C -6.78 7.51 -8 10.62 -8 13.99 C -8 18.41 -4.42 22 0 22 C 4.42 22 8 18.41 8 13.99 C 8 8.6 5.41 3.79 1.5 0.67 Z M -0.29 19 C -2.07 19 -3.51 17.6 -3.51 15.86 C -3.51 14.24 -2.46 13.1 -0.7 12.74 C 1.07 12.38 2.9 11.53 3.92 10.16 C 4.31 11.45 4.51 12.81 4.51 14.2 C 4.51 16.85 2.36 19 -0.29 19 Z' fill='{$theme[\"fire\"]}' stroke-opacity='0'/>\n                </g>\n\n                <!-- Current Streak big number -->\n                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[0]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"currStreakNum\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>\n                        {$currentStreak}\n                    </text>\n                </g>\n\n            </g>\n            <g style='isolation: isolate'>\n                <!-- Longest Streak big number -->\n                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[0]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"sideNums\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>\n                        {$longestStreak}\n                    </text>\n                </g>\n\n                <!-- Longest Streak label -->\n                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[1]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"sideLabels\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>\n                        {$longestStreakText}\n                    </text>\n                </g>\n\n                <!-- Longest Streak range -->\n                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[2]})'>\n                    <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme[\"dates\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>\n                        {$longestStreakRange}\n                    </text>\n                </g>\n            </g>\n            {$excludedDays}\n        </g>\n    </svg>\n\";\n}\n\n/**\n * Generate SVG displaying an error message\n *\n * @param string $message The error message to display\n * @param array<string,string>|NULL $params Request parameters\n * @return string The generated SVG error card\n */\nfunction generateErrorCard(string $message, array $params = null): string\n{\n    $params = $params ?? $_REQUEST;\n\n    // get requested theme, use $_REQUEST if no params array specified\n    $theme = getRequestedTheme($params);\n\n    // read border_radius parameter, default to 4.5 if not set\n    $borderRadius = $params[\"border_radius\"] ?? 4.5;\n\n    // read card_width parameter\n    $cardWidth = getCardWidth($params);\n    $rectWidth = $cardWidth - 1;\n    $centerOffset = $cardWidth / 2;\n\n    // read card_height parameter\n    $cardHeight = getCardHeight($params);\n    $rectHeight = $cardHeight - 1;\n    $heightOffset = ($cardHeight - 195) / 2;\n    $errorLabelOffset = $cardHeight / 2 + 10.5;\n\n    return \"<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px'>\n        <style>\n            a {\n                fill: {$theme[\"dates\"]};\n            }\n        </style>\n        <defs>\n            <clipPath id='outer_rectangle'>\n                <rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>\n            </clipPath>\n            {$theme[\"backgroundGradient\"]}\n        </defs>\n        <g clip-path='url(#outer_rectangle)'>\n            <g style='isolation: isolate'>\n                <rect stroke='{$theme[\"border\"]}' fill='{$theme[\"background\"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>\n            </g>\n            <g style='isolation: isolate'>\n                <!-- Error lable -->\n                <g transform='translate({$centerOffset}, {$errorLabelOffset})'>\n                    <text x='0' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' fill='{$theme[\"sideLabels\"]}' stroke='none' font-family='\\\"Segoe UI\\\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal'>\n                        {$message}\n                    </text>\n                </g>\n\n                <!-- Mask for background behind face -->\n                <defs>\n                    <mask id='cut-off-area'>\n                        <rect x='0' y='0' width='500' height='500' fill='white' />\n                        <ellipse cx='{$centerOffset}' cy='31' rx='13' ry='18'/>\n                    </mask>\n                </defs>\n                <!-- Sad face -->\n                <g transform='translate({$centerOffset}, {$heightOffset})'>\n                    <path fill='{$theme[\"fire\"]}' d='M0,35.8c-25.2,0-45.7,20.5-45.7,45.7s20.5,45.8,45.7,45.8s45.7-20.5,45.7-45.7S25.2,35.8,0,35.8z M0,122.3c-11.2,0-21.4-4.5-28.8-11.9c-2.9-2.9-5.4-6.3-7.4-10c-3-5.7-4.6-12.1-4.6-18.9c0-22.5,18.3-40.8,40.8-40.8 c10.7,0,20.4,4.1,27.7,10.9c3.8,3.5,6.9,7.7,9.1,12.4c2.6,5.3,4,11.3,4,17.6C40.8,104.1,22.5,122.3,0,122.3z'/>\n                    <path fill='{$theme[\"fire\"]}' d='M4.8,93.8c5.4,1.1,10.3,4.2,13.7,8.6l3.9-3c-4.1-5.3-10-9-16.6-10.4c-10.6-2.2-21.7,1.9-28.3,10.4l3.9,3 C-13.1,95.3-3.9,91.9,4.8,93.8z'/>\n                    <circle fill='{$theme[\"fire\"]}' cx='-15' cy='71' r='4.9'/>\n                    <circle fill='{$theme[\"fire\"]}' cx='15' cy='71' r='4.9'/>\n                </g>\n            </g>\n        </g>\n    </svg>\n\";\n}\n\n/**\n * Remove animations from SVG\n *\n * @param string $svg The SVG for the card as a string\n * @return string The SVG without animations\n */\nfunction removeAnimations(string $svg): string\n{\n    $svg = preg_replace(\"/(<style>\\X*?<\\/style>)/m\", \"\", $svg);\n    $svg = preg_replace(\"/(opacity: 0;)/m\", \"opacity: 1;\", $svg);\n    $svg = preg_replace(\"/(animation: fadein[^;'\\\"]+)/m\", \"opacity: 1;\", $svg);\n    $svg = preg_replace(\"/(animation: currstreak[^;'\\\"]+)/m\", \"font-size: 28px;\", $svg);\n    $svg = preg_replace(\"/<a \\X*?>(\\X*?)<\\/a>/m\", '\\1', $svg);\n    return $svg;\n}\n\n/**\n * Convert a color from hex 3/4/6/8 digits to hex 6 digits and opacity (0-1)\n *\n * @param string $color The color to convert\n * @return array<string, string> The converted color\n */\nfunction convertHexColor(string $color): array\n{\n    $color = preg_replace(\"/[^0-9a-fA-F]/\", \"\", $color);\n\n    // double each character if the color is in 3/4 digit format\n    if (strlen($color) === 3) {\n        $chars = str_split($color);\n        $color = \"{$chars[0]}{$chars[0]}{$chars[1]}{$chars[1]}{$chars[2]}{$chars[2]}\";\n    } elseif (strlen($color) === 4) {\n        $chars = str_split($color);\n        $color = \"{$chars[0]}{$chars[0]}{$chars[1]}{$chars[1]}{$chars[2]}{$chars[2]}{$chars[3]}{$chars[3]}\";\n    }\n\n    // convert to 6 digit hex and opacity\n    if (strlen($color) === 6) {\n        return [\n            \"color\" => \"#{$color}\",\n            \"opacity\" => 1,\n        ];\n    } elseif (strlen($color) === 8) {\n        return [\n            \"color\" => \"#\" . substr($color, 0, 6),\n            \"opacity\" => hexdec(substr($color, 6, 2)) / 255,\n        ];\n    }\n    throw new AssertionError(\"Invalid color: \" . $color);\n}\n\n/**\n * Convert transparent hex colors (4/8 digits) in an SVG to hex 6 digits and corresponding opacity attribute (0-1)\n *\n * @param string $svg The SVG for the card as a string\n * @return string The SVG with converted colors\n */\nfunction convertHexColors(string $svg): string\n{\n    // convert \"transparent\" to \"#0000\"\n    $svg = preg_replace(\"/(fill|stroke)=['\\\"]transparent['\\\"]/m\", '\\1=\"#0000\"', $svg);\n\n    // convert hex colors to 6 digits and corresponding opacity attribute\n    $svg = preg_replace_callback(\n        \"/(fill|stroke|stop-color)=['\\\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\\\"]/m\",\n        function ($matches) {\n            $attribute = $matches[1];\n            $opacityAttribute = $attribute === \"stop-color\" ? \"stop-opacity\" : \"{$attribute}-opacity\";\n            $result = convertHexColor($matches[2]);\n            $color = $result[\"color\"];\n            $opacity = $result[\"opacity\"];\n            return \"{$attribute}='{$color}' {$opacityAttribute}='{$opacity}'\";\n        },\n        $svg,\n    );\n\n    return $svg;\n}\n\n/**\n * Converts an SVG card to a PNG image\n *\n * @param string $svg The SVG for the card as a string\n * @param int $cardWidth The width of the card\n * @return string The generated PNG data\n */\nfunction convertSvgToPng(string $svg, int $cardWidth, int $cardHeight): string\n{\n    // trim off all whitespaces to make it a valid SVG string\n    $svg = trim($svg);\n\n    // remove style and animations\n    $svg = removeAnimations($svg);\n\n    // replace newlines with spaces\n    $svg = str_replace(\"\\n\", \" \", $svg);\n\n    // escape svg for shell\n    $svg = escapeshellarg($svg);\n\n    // `--pipe`: read input from pipe (stdin)\n    // `--export-filename -`: write output to stdout\n    // `-w 495 -h 195`: set width and height of the output image\n    // `--export-type png`: set the output format to PNG\n    $cmd = \"echo {$svg} | inkscape --pipe --export-filename - -w {$cardWidth} -h {$cardHeight} --export-type png\";\n\n    // convert svg to png\n    $png = shell_exec($cmd); // skipcq: PHP-A1009\n\n    // check if the conversion was successful\n    if (empty($png)) {\n        // `2>&1`: redirect stderr to stdout\n        $error = shell_exec(\"$cmd 2>&1\"); // skipcq: PHP-A1009\n        throw new InvalidArgumentException(\"Failed to convert SVG to PNG: {$error}\", 500);\n    }\n\n    // return the generated png\n    return $png;\n}\n\n/**\n * Return headers and response based on type\n *\n * @param string|array $output The stats (array) or error message (string) to display\n * @param array<string,string>|NULL $params Request parameters\n * @param int $errorCode The HTTP error code (used for JSON responses)\n * @return array The Content-Type header and the response body, and status code in case of an error\n */\nfunction generateOutput(string|array $output, array $params = null, int $errorCode = 200): array\n{\n    $params = $params ?? $_REQUEST;\n\n    $requestedType = $params[\"type\"] ?? \"svg\";\n\n    // output JSON data\n    if ($requestedType === \"json\") {\n        // generate array from output\n        $data = gettype($output) === \"string\" ? [\"error\" => $output, \"code\" => $errorCode] : $output;\n        return [\n            \"contentType\" => \"application/json\",\n            \"body\" => json_encode($data),\n        ];\n    }\n\n    // generate SVG card\n    $svg = gettype($output) === \"string\" ? generateErrorCard($output, $params) : generateCard($output, $params);\n\n    // some renderers such as inkscape doesn't support transparent colors in hex format, so we need to convert them\n    $svg = convertHexColors($svg);\n\n    // output PNG card\n    if ($requestedType === \"png\") {\n        try {\n            // extract width from SVG\n            $cardWidth = (int) preg_replace(\"/.*width=[\\\"'](\\d+)px[\\\"'].*/\", \"$1\", $svg);\n            $cardHeight = (int) preg_replace(\"/.*height=[\\\"'](\\d+)px[\\\"'].*/\", \"$1\", $svg);\n            $png = convertSvgToPng($svg, $cardWidth, $cardHeight);\n            return [\n                \"contentType\" => \"image/png\",\n                \"body\" => $png,\n            ];\n        } catch (Exception $e) {\n            return [\n                \"contentType\" => \"image/svg+xml\",\n                \"status\" => 500,\n                \"body\" => generateErrorCard($e->getMessage(), $params),\n            ];\n        }\n    }\n\n    // remove animations if disable_animations is set\n    if (isset($params[\"disable_animations\"]) && $params[\"disable_animations\"] == \"true\") {\n        $svg = removeAnimations($svg);\n    }\n\n    // output SVG card\n    return [\n        \"contentType\" => \"image/svg+xml\",\n        \"body\" => $svg,\n    ];\n}\n\n/**\n * Set headers and output response\n *\n * @param string|array $output The Content-Type header and the response body\n * @param int $responseCode The HTTP response code to send (stored for JSON consumers but always returns 200 for images)\n * @return void The function exits after sending the response\n */\nfunction renderOutput(string|array $output, int $responseCode = 200): void\n{\n    $response = generateOutput($output, null, $responseCode);\n    // Always return HTTP 200 for SVG/PNG so GitHub's image proxy (Camo) displays error cards\n    // instead of broken images. The original error code is included in JSON responses.\n    http_response_code(200);\n    header(\"Content-Type: {$response[\"contentType\"]}\");\n    exit($response[\"body\"]);\n}\n"
  },
  {
    "path": "src/colors.php",
    "content": "<?php\n\n// return a list of valid CSS colors\nreturn [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"transparent\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n];\n"
  },
  {
    "path": "src/demo/css/style.css",
    "content": "html {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n  -webkit-box-sizing: inherit;\n  -moz-box-sizing: inherit;\n  box-sizing: inherit;\n}\n\n:root {\n  --background: #eee;\n  --card-background: white;\n  --text: #1a1a1a;\n  --border: #ccc;\n  --stroke: #a9a9a9;\n  --blue-light: #2196f3;\n  --blue-transparent: #2196f3aa;\n  --blue-dark: #1e88e5;\n  --button-outline: black;\n  --red: #ff6464;\n  --yellow: #ffee58;\n  --yellow-light: #fffde7;\n}\n\n[data-theme=\"dark\"] {\n  --background: #090d13;\n  --card-background: #0d1117;\n  --text: #efefef;\n  --border: #2a2e34;\n  --stroke: #737373;\n  --blue-light: #1976d2;\n  --blue-transparent: #2196f320;\n  --blue-dark: #1565c0;\n  --button-outline: black;\n  --red: #ff6464;\n  --yellow: #a59809;\n  --yellow-light: #716800;\n}\n\nbody {\n  background: var(--background);\n  font-family:\n    Segoe UI,\n    Ubuntu,\n    sans-serif;\n  padding-top: 10px;\n  color: var(--text);\n}\n\n.github {\n  text-align: center;\n  margin-bottom: 12px;\n}\n\n.github span {\n  margin: 0 2px;\n}\n\n.container {\n  margin: auto;\n  max-width: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n}\n\n.properties,\n.output {\n  max-width: 550px;\n  margin: 10px;\n  background: var(--card-background);\n  padding: 25px;\n  padding-top: 0;\n  border: 1px solid var(--border);\n  border-radius: 6px;\n}\n\n@media only screen and (max-width: 1024px) {\n  .properties,\n  .output {\n    width: 100%;\n  }\n}\n\nh1 {\n  text-align: center;\n}\n\nh2 {\n  border-bottom: 1px solid var(--stroke);\n}\n\n:not(.btn):focus {\n  outline: var(--blue-light) auto 2px;\n}\n\n.btn {\n  max-width: 100%;\n  background-color: var(--blue-light);\n  color: white;\n  padding: 10px 20px;\n  border: none;\n  border-radius: 6px;\n  cursor: pointer;\n  font-family: inherit;\n  box-shadow:\n    0 1px 3px rgba(0, 0, 0, 0.12),\n    0 1px 2px rgba(0, 0, 0, 0.24);\n  transition: 0.2s ease-in-out;\n}\n\n.btn:focus {\n  outline: var(--button-outline) auto 2px;\n}\n\n.btn:hover {\n  background-color: var(--blue-dark);\n  box-shadow:\n    0 3px 6px rgba(0, 0, 0, 0.16),\n    0 3px 6px rgba(0, 0, 0, 0.23);\n}\n\n.btn:disabled {\n  background: var(--blue-transparent);\n  box-shadow: none;\n  cursor: not-allowed;\n}\n\n.parameters {\n  margin: auto;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  align-items: center;\n  justify-content: start;\n  text-align: left;\n  grid-gap: 8px;\n}\n\n.parameters .btn {\n  margin-top: 8px;\n}\n\n.parameters input[type=\"text\"],\n.parameters input[type=\"number\"],\n.parameters input.jscolor,\n.parameters select {\n  padding: 10px 14px;\n  display: inline-block;\n  border: 1px solid var(--border);\n  border-radius: 6px;\n  box-sizing: border-box;\n  font-family: inherit;\n  background: var(--card-background);\n  width: 100%;\n  color: inherit;\n}\n\n.parameters input.jscolor {\n  font-size: 12px;\n  max-width: 130px;\n}\n\n@media only screen and (max-width: 1024px) {\n  .parameters input.jscolor {\n    max-width: none;\n  }\n}\n\n.parameters select {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  background-image: url(\"data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>\");\n  background-repeat: no-repeat;\n  background-position-x: 100%;\n  background-position-y: 5px;\n}\n\n[data-theme=\"dark\"] .parameters select {\n  background-image: url(\"data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>\");\n}\n\n.parameters select option[disabled] {\n  display: none;\n}\n\n.parameters label,\n.parameters span[data-property] {\n  text-transform: capitalize;\n}\n\n.checkbox-buttons input {\n  display: none !important;\n}\n\n.checkbox-buttons input[type=\"checkbox\"] + label {\n  font-size: 90%;\n  display: inline-block;\n  border-radius: 6px;\n  height: 30px;\n  margin: 2px 3px 2px 0;\n  line-height: 28px;\n  text-align: center;\n  cursor: pointer;\n  background: var(--card-background);\n  color: var(--text);\n  border: 1px solid var(--border);\n  padding: 0 10px;\n}\n\n.checkbox-buttons input[type=\"checkbox\"]:checked + label {\n  background: var(--text);\n  color: var(--background);\n}\n\n.checkbox-buttons input[type=\"checkbox\"]:disabled + label {\n  background: var(--card-background);\n  color: var(--stroke);\n}\n\nspan[title=\"required\"] {\n  color: var(--red);\n}\n\ninput:focus:invalid {\n  outline: 2px var(--red) auto;\n}\n\n.advanced {\n  grid-column-start: 1;\n  grid-column-end: -1;\n}\n\n.advanced summary {\n  padding: 6px;\n  cursor: pointer;\n}\n\n.advanced .parameters {\n  margin-top: 8px;\n}\n\n.radio-button-group {\n  display: flex;\n  align-items: center;\n  gap: 0.75em;\n}\n\n.advanced .color-properties {\n  grid-template-columns: auto 1fr auto;\n}\n\n.advanced .grid-middle {\n  display: grid;\n  grid-template-columns: 30% 35% 35%;\n}\n\n.input-text-group {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 0.25em;\n}\n\n.input-text-group span {\n  font-size: 0.8em;\n  font-weight: bold;\n  padding-right: 1.5em;\n}\n\n.advanced .color-properties label:first-of-type {\n  font-weight: bold;\n}\n\n.plus.btn,\n.minus.btn {\n  margin: 0;\n  background: inherit;\n  color: inherit;\n  font-size: 22px;\n  padding: 0;\n  height: 40px;\n  width: 40px;\n}\n\n.plus.btn:hover,\n.minus.btn:hover {\n  background: var(--background);\n}\n\n.output img {\n  max-width: 100%;\n}\n\n.output .warning {\n  font-size: 0.84em;\n  background: var(--yellow-light);\n  padding: 8px 14px;\n  border-left: 4px solid var(--yellow);\n}\n\n.output .code-container,\n.output .json {\n  background: var(--border);\n  border-radius: 6px;\n  padding: 12px 16px;\n  word-break: break-all;\n}\n\n.output .btn {\n  margin-top: 16px;\n}\n\n.top-bottom-split {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n}\n\n/* tooltips */\n.btn.tooltip {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n/* tooltip bubble */\n.btn.tooltip:before {\n  content: attr(title);\n  position: absolute;\n  transform: translateY(-45px);\n  height: auto;\n  width: auto;\n  background: #4a4a4afa;\n  border-radius: 4px;\n  color: white;\n  line-height: 30px;\n  font-size: 1em;\n  padding: 0 12px;\n  pointer-events: none;\n  opacity: 0;\n}\n\n/* tooltip bottom triangle */\n.btn.tooltip:after {\n  content: \"\";\n  position: absolute;\n  transform: translateY(-25px);\n  border-style: solid;\n  border-color: #4a4a4afa transparent transparent transparent;\n  border-width: 5px;\n  pointer-events: none;\n  opacity: 0;\n}\n\n/* show tooltip on hover */\n.btn.tooltip[title]:hover:before,\n.btn.tooltip[title]:hover:after,\n.btn.tooltip:disabled:hover:before,\n.btn.tooltip:disabled:hover:after {\n  transition: 0.2s ease-in opacity;\n  opacity: 1;\n}\n\n.btn.tooltip:disabled:before {\n  content: \"You must first input a valid username.\";\n}\n\ntextarea#exported-php {\n  margin-top: 10px;\n  width: 100%;\n  resize: vertical;\n  height: 100px;\n}\n\n/* link underline effect */\na.underline-hover {\n  position: relative;\n  text-decoration: none;\n  color: var(--text);\n  margin-top: 2em;\n  display: inline-flex;\n  align-items: center;\n  gap: 0.25em;\n}\n.underline-hover::before {\n  content: \"\";\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  width: 0;\n  height: 1px;\n  background-color: var(--blue-light);\n  transition: width 0.4s cubic-bezier(0.25, 1, 0.5, 1);\n}\n@media (hover: hover) and (pointer: fine) {\n  .underline-hover:hover::before {\n    left: 0;\n    right: auto;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "src/demo/css/toggle-dark.css",
    "content": "a.darkmode {\n  position: fixed;\n  top: 2em;\n  right: 2em;\n  color: var(--text);\n  background: var(--background);\n  height: 3em;\n  width: 3em;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 50%;\n  border: 2px solid var(--border);\n  box-shadow:\n    0 0 3px rgb(0 0 0 / 12%),\n    0 1px 2px rgb(0 0 0 / 24%);\n  transition: 0.2s ease-in box-shadow;\n  text-decoration: none;\n}\n\na.darkmode:hover {\n  box-shadow:\n    0 0 6px rgb(0 0 0 / 16%),\n    0 3px 6px rgb(0 0 0 / 23%);\n}\n\n@media only screen and (max-width: 600px) {\n  a.darkmode {\n    top: unset;\n    bottom: 1em;\n    right: 1em;\n  }\n}\n"
  },
  {
    "path": "src/demo/index.php",
    "content": "<?php\n\n$THEMES = include \"../themes.php\";\n$TRANSLATIONS = include \"../translations.php\";\n// Get the keys of the first value in the translations array\n// and filter to only include locales that have an array as the value\n$LOCALES = array_filter(array_keys($TRANSLATIONS), function ($locale) use ($TRANSLATIONS) {\n    return is_array($TRANSLATIONS[$locale]);\n});\n\n$darkmode = $_COOKIE[\"darkmode\"] ?? null;\n\n/**\n * Convert a camelCase string to a skewer-case string\n * @param string $str The camelCase string\n * @return string The skewer-case string\n */\nfunction camelToSkewer(string $str): string\n{\n    return preg_replace_callback(\n        \"/([A-Z])/\",\n        function ($matches) {\n            return \"-\" . strtolower($matches[0]);\n        },\n        $str,\n    );\n}\n?>\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <!-- Global site tag (gtag.js) - Google Analytics -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-48CYVH0XEF\"></script>\n    <script>\n        window.dataLayer = window.dataLayer || [];\n\n        function gtag() {\n            dataLayer.push(arguments);\n        }\n        gtag('js', new Date());\n        gtag('config', 'G-48CYVH0XEF');\n    </script>\n    <title>GitHub Readme Streak Stats Demo</title>\n    <link rel=\"stylesheet\" href=\"./css/style.css?v=<?= filemtime(\"./css/style.css\") ?>\">\n    <link rel=\"stylesheet\" href=\"./css/toggle-dark.css?v=<?= filemtime(\"./css/toggle-dark.css\") ?>\">\n\n    <!-- Favicons -->\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"apple-touch-icon.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"favicon-16x16.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"favicon-32x32.png\">\n    <link rel=\"mask-icon\" href=\"icon.svg\" color=\"#fb8c00\">\n\n    <script type=\"text/javascript\" src=\"./js/script.js?v=<?= filemtime(\"./js/script.js\") ?>\" defer></script>\n    <script type=\"text/javascript\" src=\"./js/accordion.js?v=<?= filemtime(\"./js/accordion.js\") ?>\" defer></script>\n    <script type=\"text/javascript\" src=\"./js/toggle-dark.js?v=<?= filemtime(\"./js/toggle-dark.js\") ?>\" defer></script>\n    <script type=\"text/javascript\" src=\"./js/jscolor.min.js?v=<?= filemtime(\"./js/jscolor.min.js\") ?>\" defer></script>\n    <script async defer src=\"https://buttons.github.io/buttons.js\"></script>\n</head>\n\n<body <?php echo $darkmode === \"on\" ? 'data-theme=\"dark\"' : \"\"; ?>>\n    <h1>🔥 GitHub Readme Streak Stats</h1>\n\n    <!-- GitHub badges/links section -->\n    <div class=\"github\">\n        <!-- GitHub Sponsors -->\n        <a class=\"github-button\" href=\"https://github.com/sponsors/denvercoder1\" data-color-scheme=\"no-preference: light; light: light; dark: dark;\" data-icon=\"octicon-heart\" data-size=\"large\" aria-label=\"Sponsor @denvercoder1 on GitHub\">Sponsor</a>\n        <!-- View on GitHub -->\n        <a class=\"github-button\" href=\"https://github.com/denvercoder1/github-readme-streak-stats\" data-color-scheme=\"no-preference: light; light: light; dark: dark;\" data-size=\"large\" aria-label=\"View denvercoder1/github-readme-streak-stats on GitHub\">View on GitHub</a>\n        <!-- GitHub Star -->\n        <a class=\"github-button\" href=\"https://github.com/denvercoder1/github-readme-streak-stats\" data-color-scheme=\"no-preference: light; light: light; dark: dark;\" data-icon=\"octicon-star\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Star denvercoder1/github-readme-streak-stats on GitHub\">Star</a>\n    </div>\n\n    <div class=\"container\">\n        <div class=\"properties\">\n            <h2>Properties</h2>\n            <form class=\"parameters\">\n                <label for=\"user\">Username<span title=\"required\">*</span></label>\n                <input class=\"param\" type=\"text\" id=\"user\" name=\"user\" placeholder=\"DenverCoder1\" pattern=\"^[A-Za-z\\d-]{0,39}[A-Za-z\\d]$\" title=\"Up to 40 letters or hyphens but not ending with hyphen\" />\n\n                <label for=\"theme\">Theme</label>\n                <select class=\"param\" id=\"theme\" name=\"theme\">\n                    <?php foreach ($THEMES as $theme => $options): ?>\n                        <?php\n                        $dataAttrs = \"\";\n                        foreach ($options as $key => $value) {\n                            // convert key from camelCase to skewer-case\n                            $key = camelToSkewer($key);\n                            // remove '#' from hex color value\n                            $value = preg_replace(\"/^\\#/\", \"\", $value);\n                            // add data attribute\n                            $dataAttrs .= \"data-\" . $key . \"=\\\"\" . $value . \"\\\" \";\n                        }\n                        ?>\n                        <option value=\"<?php echo $theme; ?>\" <?php echo $dataAttrs; ?>><?php echo $theme; ?></option>\n                    <?php endforeach; ?>\n                </select>\n\n                <label for=\"hide-border\">Hide Border</label>\n                <select class=\"param\" id=\"hide-border\" name=\"hide_border\">\n                    <option>false</option>\n                    <option>true</option>\n                </select>\n\n                <label for=\"border-radius\">Border Radius</label>\n                <input class=\"param\" type=\"number\" id=\"border-radius\" name=\"border_radius\" placeholder=\"4.5\" value=\"4.5\" step=\"0.1\" min=\"0\" />\n\n                <label for=\"locale\">Locale</label>\n                <select class=\"param\" id=\"locale\" name=\"locale\">\n                    <?php foreach ($LOCALES as $locale): ?>\n                        <option value=\"<?php echo $locale; ?>\">\n                            <?php $display = Locale::getDisplayName($locale, $locale); ?>\n                            <?php echo $display . \" (\" . $locale . \")\"; ?>\n                        </option>\n                    <?php endforeach; ?>\n                </select>\n\n                <label for=\"short-numbers\">Short Numbers</label>\n                <select class=\"param\" id=\"short-numbers\" name=\"short_numbers\">\n                    <option>false</option>\n                    <option>true</option>\n                </select>\n\n                <label for=\"date-format\">Date Format</label>\n                <select class=\"param\" id=\"date-format\" name=\"date_format\">\n                    <option value=\"\">default</option>\n                    <option value=\"M j[, Y]\">Aug 10, 2016</option>\n                    <option value=\"j M[ Y]\">10 Aug 2016</option>\n                    <option value=\"[Y ]M j\">2016 Aug 10</option>\n                    <option value=\"j/n[/Y]\">10/8/2016</option>\n                    <option value=\"n/j[/Y]\">8/10/2016</option>\n                    <option value=\"[Y.]n.j\">2016.8.10</option>\n                </select>\n\n                <label for=\"mode\">Streak Mode</label>\n                <select class=\"param\" id=\"mode\" name=\"mode\">\n                    <option value=\"daily\">Daily</option>\n                    <option value=\"weekly\">Weekly</option>\n                </select>\n\n                <span id=\"exclude-days-label\">Exclude Days</span>\n                <div class=\"checkbox-buttons weekdays\" role=\"group\" aria-labelledby=\"exclude-days-label\">\n                    <input type=\"checkbox\" value=\"Sun\" id=\"weekday-sun\" />\n                    <label for=\"weekday-sun\" data-tooltip=\"Exclude Sunday\" title=\"Exclude Sunday\">S</label>\n                    <input type=\"checkbox\" value=\"Mon\" id=\"weekday-mon\" />\n                    <label for=\"weekday-mon\" data-tooltip=\"Exclude Monday\" title=\"Exclude Monday\">M</label>\n                    <input type=\"checkbox\" value=\"Tue\" id=\"weekday-tue\" />\n                    <label for=\"weekday-tue\" data-tooltip=\"Exclude Tuesday\" title=\"Exclude Tuesday\">T</label>\n                    <input type=\"checkbox\" value=\"Wed\" id=\"weekday-wed\" />\n                    <label for=\"weekday-wed\" data-tooltip=\"Exclude Wednesday\" title=\"Exclude Wednesday\">W</label>\n                    <input type=\"checkbox\" value=\"Thu\" id=\"weekday-thu\" />\n                    <label for=\"weekday-thu\" data-tooltip=\"Exclude Thursday\" title=\"Exclude Thursday\">T</label>\n                    <input type=\"checkbox\" value=\"Fri\" id=\"weekday-fri\" />\n                    <label for=\"weekday-fri\" data-tooltip=\"Exclude Friday\" title=\"Exclude Friday\">F</label>\n                    <input type=\"checkbox\" value=\"Sat\" id=\"weekday-sat\" />\n                    <label for=\"weekday-sat\" data-tooltip=\"Exclude Saturday\" title=\"Exclude Saturday\">S</label>\n                    <input type=\"hidden\" id=\"exclude-days\" name=\"exclude_days\" class=\"param\" />\n                </div>\n\n                <span id=\"show-sections-label\">Show Sections</span>\n                <div class=\"checkbox-buttons sections\" role=\"group\" aria-labelledby=\"show-sections-label\">\n                    <input type=\"checkbox\" value=\"total\" id=\"section-total\" checked />\n                    <label for=\"section-total\" data-tooltip=\"Total Contributions\" title=\"Total Contributions\">Total</label>\n                    <input type=\"checkbox\" value=\"current\" id=\"section-current\" checked />\n                    <label for=\"section-current\" data-tooltip=\"Current Streak\" title=\"Current Streak\">Current</label>\n                    <input type=\"checkbox\" value=\"longest\" id=\"section-longest\" checked />\n                    <label for=\"section-longest\" data-tooltip=\"Longest Streak\" title=\"Longest Streak\">Longest</label>\n                    <input type=\"hidden\" id=\"sections\" name=\"sections\" class=\"param\" value=\"total,current,longest\" />\n                </div>\n\n                <label for=\"card-width\">Card Width</label>\n                <input class=\"param\" type=\"number\" id=\"card-width\" name=\"card_width\" placeholder=\"495\" value=\"495\" step=\"1\" min=\"300\" />\n\n                <label for=\"card-width\">Card Height</label>\n                <input class=\"param\" type=\"number\" id=\"card-width\" name=\"card_height\" placeholder=\"195\" value=\"195\" step=\"1\" min=\"170\" />\n\n                <label for=\"type\">Output Type</label>\n                <select class=\"param\" id=\"type\" name=\"type\">\n                    <option value=\"svg\">SVG</option>\n                    <option value=\"png\">PNG</option>\n                    <option value=\"json\">JSON</option>\n                </select>\n\n                <details class=\"advanced\">\n                    <summary>⚙ Advanced Options</summary>\n                    <div class=\"content\">\n                        <div class=\"radio-buttons parameters\" role=\"radiogroup\" aria-labelledby=\"background-type-label\">\n                            <!-- Radio buttons to choose between solid and gradient background -->\n                            <span id=\"background-type-label\">Background Type</span>\n                            <div class=\"radio-button-group\">\n                                <div>\n                                    <input type=\"radio\" id=\"background-type-solid\" name=\"background-type\" value=\"solid\" checked />\n                                    <label for=\"background-type-solid\">Solid</label>\n                                </div>\n                                <div>\n                                    <input type=\"radio\" id=\"background-type-gradient\" name=\"background-type\" value=\"gradient\" />\n                                    <label for=\"background-type-gradient\">Gradient</label>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"color-properties parameters\">\n                            <label for=\"properties\">Add Property</label>\n                            <select id=\"properties\" name=\"properties\">\n                                <?php foreach ($THEMES[\"default\"] as $option => $color): ?>\n                                    <option><?php echo $option; ?></option>\n                                <?php endforeach; ?>\n                            </select>\n                            <button class=\"plus btn\" type=\"button\" onclick=\"preview.addProperty()\">+</button>\n                        </div>\n                    </div>\n                    <button class=\"btn\" type=\"button\" onclick=\"preview.exportPhp()\">Export to PHP</button>\n                    <button id=\"clear-button\" class=\"btn\" type=\"button\" onclick=\"preview.removeAllProperties()\" disabled>Clear Options</button>\n                    <textarea id=\"exported-php\" hidden></textarea>\n                </details>\n\n                <button class=\"btn\" type=\"submit\">Open Permalink</button>\n            </form>\n        </div>\n\n        <div class=\"output top-bottom-split\">\n            <div class=\"top\">\n                <h2>Preview</h2>\n                <img alt=\"GitHub Readme Streak Stats\" src=\"preview.php?user=\" />\n                <div class=\"json\" style=\"display: none;\">\n                    <pre></pre>\n                </div>\n                <p class=\"warning\">\n                    Note: The stats above are just examples and not from your GitHub profile.\n                </p>\n                \n                <div>\n                    <h2>Markdown</h2>\n                    <div class=\"code-container md\">\n                        <code></code>\n                    </div>\n                    \n                    <button class=\"copy-button btn tooltip copy-md\" onclick=\"clipboard.copy(this);\" onmouseout=\"tooltip.reset(this);\" disabled>\n                        Copy To Clipboard\n                    </button>\n                </div>\n                \n                <div>\n                    <h2>HTML</h2>\n                    <div class=\"code-container html\">\n                        <code></code>\n                    </div>\n                    \n                    <button class=\"copy-button btn tooltip copy-html\" onclick=\"clipboard.copy(this);\" onmouseout=\"tooltip.reset(this);\" disabled>\n                        Copy To Clipboard\n                    </button>\n                </div>\n\n                <div>\n                    <h2>JSON</h2>\n                    <div class=\"code-container json\">\n                        <code></code>\n                    </div>\n                    \n                    <button class=\"copy-button btn tooltip copy-json\" onclick=\"clipboard.copy(this);\" onmouseout=\"tooltip.reset(this);\" disabled>\n                        Copy To Clipboard\n                    </button>\n                </div>\n            </div>\n            <div class=\"bottom\">\n                <a href=\"https://github.com/DenverCoder1/github-readme-streak-stats/blob/main/docs/faq.md\" target=\"_blank\" class=\"underline-hover faq\">\n                    Frequently Asked Questions\n                    <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 24 24\" height=\"1em\" width=\"1em\" xmlns=\"http://www.w3.org/2000/svg\">\n                        <g>\n                            <path fill=\"none\" d=\"M0 0h24v24H0z\"></path>\n                            <path d=\"M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v9l-3.794-3.793-5.999 6-1.414-1.414 5.999-6L12 3h9z\"></path>\n                        </g>\n                    </svg>\n                </a>\n            </div>\n        </div>\n    </div>\n\n    <a href=\"javascript:toggleTheme()\" class=\"darkmode\" title=\"toggle dark mode\">\n         <span id=\"darkmode-icon\">\n            <?php if ($darkmode === \"on\"): ?>\n                🌞\n            <?php else: ?>\n                🌙\n            <?php endif; ?>\n         </span>\n    </a>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/demo/js/accordion.js",
    "content": "// Based on https://css-tricks.com/how-to-animate-the-details-element-using-waapi/\nclass Accordion {\n  constructor(el) {\n    // Store the <details> element\n    this.el = el;\n    // Store the <summary> element\n    this.summary = el.querySelector(\"summary\");\n    // Store the <div class=\"content\"> element\n    this.content = el.querySelector(\".content\");\n    // Store the animation object (so we can cancel it if needed)\n    this.animation = null;\n    // Store if the element is closing\n    this.isClosing = false;\n    // Store if the element is expanding\n    this.isExpanding = false;\n  }\n\n  init() {\n    // Detect user clicks on the summary element\n    this.summary.addEventListener(\"click\", (e) => this.onClick(e));\n  }\n\n  onClick(e) {\n    // Stop default behaviour from the browser\n    e.preventDefault();\n    // Add an overflow on the <details> to avoid content overflowing\n    this.el.style.overflow = \"hidden\";\n    // Check if the element is being closed or is already closed\n    if (this.isClosing || !this.el.open) {\n      this.open();\n      // Check if the element is being openned or is already open\n    } else if (this.isExpanding || this.el.open) {\n      this.shrink();\n    }\n  }\n\n  shrink() {\n    // Set the element as \"being closed\"\n    this.isClosing = true;\n    // Store the current height of the element\n    const startHeight = `${this.el.offsetHeight}px`;\n    // Calculate the height of the summary\n    const endHeight = `${this.summary.offsetHeight}px`;\n    // If there is already an animation running\n    if (this.animation) {\n      // Cancel the current animation\n      this.animation.cancel();\n    }\n    // Start a WAAPI animation\n    this.animation = this.el.animate(\n      {\n        // Set the keyframes from the startHeight to endHeight\n        height: [startHeight, endHeight],\n      },\n      {\n        duration: 400,\n        easing: \"ease-out\",\n      },\n    );\n    // When the animation is complete, call onAnimationFinish()\n    this.animation.onfinish = () => this.onAnimationFinish(false);\n    // If the animation is cancelled, isClosing variable is set to false\n    this.animation.oncancel = () => (this.isClosing = false);\n  }\n\n  open() {\n    // Apply a fixed height on the element\n    this.el.style.height = `${this.el.offsetHeight}px`;\n    // Force the [open] attribute on the details element\n    this.el.open = true;\n    // Wait for the next frame to call the expand function\n    window.requestAnimationFrame(() => this.expand());\n  }\n\n  expand() {\n    // Set the element as \"being expanding\"\n    this.isExpanding = true;\n    // Get the current fixed height of the element\n    const startHeight = `${this.el.offsetHeight}px`;\n    // Calculate the open height of the element (summary height + content height)\n    const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;\n    // If there is already an animation running\n    if (this.animation) {\n      // Cancel the current animation\n      this.animation.cancel();\n    }\n    // Start a WAAPI animation\n    this.animation = this.el.animate(\n      {\n        // Set the keyframes from the startHeight to endHeight\n        height: [startHeight, endHeight],\n      },\n      {\n        duration: 400,\n        easing: \"ease-out\",\n      },\n    );\n    // When the animation is complete, call onAnimationFinish()\n    this.animation.onfinish = () => this.onAnimationFinish(true);\n    // If the animation is cancelled, isExpanding variable is set to false\n    this.animation.oncancel = () => (this.isExpanding = false);\n  }\n\n  onAnimationFinish(open) {\n    // Set the open attribute based on the parameter\n    this.el.open = open;\n    // Clear the stored animation\n    this.animation = null;\n    // Reset isClosing & isExpanding\n    this.isClosing = false;\n    this.isExpanding = false;\n    // Remove the overflow hidden and the fixed height\n    this.el.style.height = this.el.style.overflow = \"\";\n  }\n}\n\ndocument.querySelectorAll(\"details\").forEach((el) => {\n  const accordion = new Accordion(el);\n  accordion.init();\n});\n"
  },
  {
    "path": "src/demo/js/script.js",
    "content": "/*global jscolor*/\n/*eslint no-undef: \"error\"*/\n\nconst preview = {\n  /**\n   * Default values - if set to these values, the params do not need to appear in the query string\n   */\n  defaults: {\n    theme: \"default\",\n    hide_border: \"false\",\n    date_format: \"\",\n    locale: \"en\",\n    border_radius: \"4.5\",\n    mode: \"daily\",\n    type: \"svg\",\n    exclude_days: \"\",\n    card_width: \"495\",\n    card_height: \"195\",\n    hide_total_contributions: \"false\",\n    hide_current_streak: \"false\",\n    hide_longest_streak: \"false\",\n    short_numbers: \"false\",\n  },\n\n  /**\n   * Update the preview with the current parameters\n   */\n  update() {\n    // get parameter values from all .param elements\n    const params = this.objectFromElements(document.querySelectorAll(\".param\"));\n    // convert sections to hide_... parameters\n    params.hide_total_contributions = String(!params.sections.includes(\"total\"));\n    params.hide_current_streak = String(!params.sections.includes(\"current\"));\n    params.hide_longest_streak = String(!params.sections.includes(\"longest\"));\n    delete params.sections;\n    // convert parameters to query string\n    const query = Object.keys(params)\n      .filter((key) => params[key] !== this.defaults[key])\n      .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)\n      .join(\"&\");\n    // generate links and markdown\n    const imageURL = `${window.location.origin}?${query}`;\n    const demoImageURL = `preview.php?${query}`;\n    // update preview\n    if (params.type !== \"json\") {\n      const repoLink = \"https://git.io/streak-stats\";\n      const md = `[![GitHub Streak](${imageURL})](${repoLink})`;\n      const html = `<a href=\"${repoLink}\"><img src=\"${imageURL}\" alt=\"GitHub Streak\" /></a>`;\n      document.querySelector(\".output img\").src = demoImageURL;\n      document.querySelector(\".md code\").innerText = md;\n      document.querySelector(\".html code\").innerText = html;\n      document.querySelector(\".copy-md\").parentElement.style.display = \"block\";\n      document.querySelector(\".copy-html\").parentElement.style.display = \"block\";\n      document.querySelector(\".output img\").style.display = \"block\";\n      document.querySelector(\".output .json\").style.display = \"none\";\n      document.querySelector(\".copy-json\").parentElement.style.display = \"none\";\n    } else {\n      fetch(demoImageURL)\n        .then((response) => response.json())\n        .then((data) => (document.querySelector(\".output .json pre\").innerText = JSON.stringify(data, null, 2)))\n        .catch(console.error);\n      document.querySelector(\".json code\").innerText = imageURL;\n      document.querySelector(\".copy-md\").parentElement.style.display = \"none\";\n      document.querySelector(\".copy-html\").parentElement.style.display = \"none\";\n      document.querySelector(\".output img\").style.display = \"none\";\n      document.querySelector(\".output .json\").style.display = \"block\";\n      document.querySelector(\".copy-json\").parentElement.style.display = \"block\";\n    }\n    // disable copy button if username is invalid\n    const copyButtons = document.querySelectorAll(\".copy-button\");\n    copyButtons.forEach((button) => {\n      button.disabled = Boolean(document.querySelector(\"#user:invalid\") || !document.querySelector(\"#user\").value);\n    });\n    // disable clear button if no added advanced options\n    const clearButton = document.querySelector(\"#clear-button\");\n    clearButton.disabled = !document.querySelectorAll(\".minus\").length;\n  },\n\n  /**\n   * Add a property in the advanced section\n   * @param {string} property - the name of the property, selected element is used if not provided\n   * @param {string} value - the value to set the property to\n   */\n  addProperty(property, value = \"#EB5454FF\") {\n    const selectElement = document.querySelector(\"#properties\");\n    // if no property passed, get the currently selected property\n    const propertyName = property || selectElement.value;\n    if (!selectElement.disabled) {\n      // disable option in menu\n      Array.prototype.find.call(selectElement.options, (o) => o.value === propertyName).disabled = true;\n      // select first unselected option\n      const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled);\n      if (firstAvailable) {\n        firstAvailable.selected = true;\n      } else {\n        selectElement.disabled = true;\n      }\n      // color picker\n      const jscolorConfig = {\n        format: \"hexa\",\n        onChange: `preview.pickerChange(this, '${propertyName}')`,\n        onInput: `preview.pickerChange(this, '${propertyName}')`,\n      };\n\n      const parent = document.querySelector(\".advanced .color-properties\");\n      if (propertyName === \"background\" && document.querySelector(\"#background-type-gradient\").checked) {\n        const valueParts = value.split(\",\");\n        let angleValue = \"45\";\n        let color1Value = \"#EB5454FF\";\n        let color2Value = \"#EB5454FF\";\n        if (valueParts.length === 3) {\n          angleValue = valueParts[0];\n          color1Value = valueParts[1];\n          color2Value = valueParts[2];\n        }\n\n        const input = document.createElement(\"span\");\n        input.className = \"grid-middle\";\n        input.setAttribute(\"data-property\", propertyName);\n\n        const rotateInputGroup = document.createElement(\"div\");\n        rotateInputGroup.className = \"input-text-group\";\n\n        const rotate = document.createElement(\"input\");\n        rotate.className = \"param\";\n        rotate.type = \"number\";\n        rotate.id = \"rotate\";\n        rotate.placeholder = \"45\";\n        rotate.value = angleValue;\n\n        const degText = document.createElement(\"span\");\n        degText.innerText = \"\\u00B0\"; // degree symbol\n\n        rotateInputGroup.appendChild(rotate);\n        rotateInputGroup.appendChild(degText);\n\n        const color1 = document.createElement(\"input\");\n        color1.className = \"param jscolor\";\n        color1.id = \"background-color1\";\n        color1.setAttribute(\n          \"data-jscolor\",\n          JSON.stringify({\n            format: \"hexa\",\n            onChange: `preview.pickerChange(this, '${color1.id}')`,\n            onInput: `preview.pickerChange(this, '${color1.id}')`,\n          }),\n        );\n        const color2 = document.createElement(\"input\");\n        color2.className = \"param jscolor\";\n        color2.id = \"background-color2\";\n        color2.setAttribute(\n          \"data-jscolor\",\n          JSON.stringify({\n            format: \"hexa\",\n            onChange: `preview.pickerChange(this, '${color2.id}')`,\n            onInput: `preview.pickerChange(this, '${color2.id}')`,\n          }),\n        );\n        rotate.name = color1.name = color2.name = propertyName;\n        color1.value = color1Value;\n        color2.value = color2Value;\n        // label\n        const label = document.createElement(\"span\");\n        label.innerText = propertyName;\n        label.setAttribute(\"data-property\", propertyName);\n        label.id = \"background-label\";\n        input.setAttribute(\"role\", \"group\");\n        input.setAttribute(\"aria-labelledby\", \"background-label\");\n        // add elements\n        parent.appendChild(label);\n        input.appendChild(rotateInputGroup);\n        input.appendChild(color1);\n        input.appendChild(color2);\n        parent.appendChild(input);\n        // initialise jscolor on elements\n        jscolor.install(input);\n        // check initial color values\n        this.checkColor(color1.value, color1.id);\n        this.checkColor(color2.value, color2.id);\n      } else {\n        const input = document.createElement(\"input\");\n        input.className = \"param jscolor\";\n        input.id = propertyName;\n        input.name = propertyName;\n        input.setAttribute(\"data-property\", propertyName);\n        input.setAttribute(\"data-jscolor\", JSON.stringify(jscolorConfig));\n        input.value = value;\n        // label\n        const label = document.createElement(\"label\");\n        label.innerText = propertyName;\n        label.setAttribute(\"data-property\", propertyName);\n        label.setAttribute(\"for\", propertyName);\n        // add elements\n        parent.appendChild(label);\n        parent.appendChild(input);\n        // initialise jscolor on element\n        jscolor.install(parent);\n        // check initial color value\n        this.checkColor(value, propertyName);\n      }\n      // removal button\n      const minus = document.createElement(\"button\");\n      minus.className = \"minus btn\";\n      minus.setAttribute(\"onclick\", \"return preview.removeProperty(this.getAttribute('data-property'));\");\n      minus.setAttribute(\"type\", \"button\");\n      minus.innerText = \"−\";\n      minus.setAttribute(\"data-property\", propertyName);\n      parent.appendChild(minus);\n\n      // update and exit\n      this.update();\n    }\n  },\n\n  /**\n   * Remove a property from the advanced section\n   * @param {string} property - the name of the property to remove\n   */\n  removeProperty(property) {\n    const parent = document.querySelector(\".advanced .color-properties\");\n    const selectElement = document.querySelector(\"#properties\");\n    // remove all elements for given property\n    parent.querySelectorAll(`[data-property=\"${property}\"]`).forEach((x) => parent.removeChild(x));\n    // enable option in menu\n    const option = Array.prototype.find.call(selectElement.options, (o) => o.value === property);\n    selectElement.disabled = false;\n    option.disabled = false;\n    selectElement.value = option.value;\n    // update and exit\n    this.update();\n  },\n\n  /**\n   * Removes all properties from the advanced section\n   */\n  removeAllProperties() {\n    const parent = document.querySelector(\".advanced .color-properties\");\n    const activeProperties = parent.querySelectorAll(\"[data-property]\");\n    // select active and unique property names\n    const propertyNames = Array.prototype.map\n      .call(activeProperties, (prop) => prop.getAttribute(\"data-property\"))\n      .filter((value, index, self) => self.indexOf(value) === index);\n    // remove each active property name\n    propertyNames.forEach((prop) => this.removeProperty(prop));\n  },\n\n  /**\n   * Create a key-value mapping of names to values from all elements in a Node list\n   * @param {NodeList} elements - the elements to get the values from\n   * @returns {Object} the key-value mapping\n   */\n  objectFromElements(elements) {\n    return Array.from(elements).reduce((acc, next) => {\n      const obj = { ...acc };\n      let value = next.value;\n      if (value.indexOf(\"#\") >= 0) {\n        // if the value is colour, remove the hash sign\n        value = value.replace(/#/g, \"\");\n        if (value.length > 6) {\n          // if the value is in hexa and opacity is 1, remove FF\n          value = value.replace(/[Ff]{2}$/, \"\");\n        }\n      }\n      // if the property already exists, append the value to the existing one\n      if (next.name in obj) {\n        obj[next.name] = `${obj[next.name]},${value}`;\n        return obj;\n      }\n      // otherwise, add the value to the object\n      obj[next.name] = value;\n      return obj;\n    }, {});\n  },\n\n  /**\n   * Export the advanced parameters to PHP code for creating a new theme\n   */\n  exportPhp() {\n    // get default values from the currently selected theme\n    const themeSelect = document.querySelector(\"#theme\");\n    const selectedOption = themeSelect.options[themeSelect.selectedIndex];\n    const defaultParams = selectedOption.dataset;\n    // get parameters with the advanced options\n    const advancedParams = this.objectFromElements(document.querySelectorAll(\".advanced .param\"));\n    // update default values with the advanced options\n    const params = { ...defaultParams, ...advancedParams };\n    // convert parameters to PHP code\n    const mappings = Object.keys(params)\n      .map((key) => {\n        const value = params[key].includes(\",\") ? params[key] : `#${params[key]}`;\n        return `  \"${key}\" => \"${value}\",`;\n      })\n      .join(\"\\n\");\n    const output = `[\\n${mappings}\\n]`;\n    // set the textarea value to the output\n    const textarea = document.getElementById(\"exported-php\");\n    textarea.value = output;\n    textarea.hidden = false;\n  },\n\n  /**\n   * Remove \"FF\" from a hex color if opacity is 1\n   * @param {string} color - the hex color\n   * @param {string} input - the property name, or id of the element to update\n   */\n  checkColor(color, input) {\n    // if color has hex alpha value -> remove it\n    if (color.length === 9 && color.slice(-2) === \"FF\") {\n      document.querySelector(`#${input}`).value = color.slice(0, -2);\n    }\n  },\n\n  /**\n   * Check a color when the picker changes\n   * @param {Object} picker - the JSColor picker object\n   * @param {string} input - the property name, or id of the element to update\n   */\n  pickerChange(picker, input) {\n    // color was changed by picker - check it\n    this.checkColor(picker.toHEXAString(), input);\n    // update preview\n    this.update();\n  },\n\n  /**\n   * Update checkboxes based on the query string parameter\n   *\n   * @param {string|null} param - the query string parameter to read\n   * @param {string} selector - the selector of the parent container to find the checkboxes\n   */\n  updateCheckboxes(param, selector) {\n    if (!param) {\n      return;\n    }\n    // uncheck all checkboxes\n    [...document.querySelectorAll(`${selector} input[value]`)].forEach((checkbox) => {\n      checkbox.checked = false;\n    });\n    // check checkboxes based on values in the query string\n    param.split(\",\").forEach((value) => {\n      const checkbox = document.querySelector(`${selector} input[value=\"${value}\"]`);\n      if (checkbox) {\n        checkbox.checked = true;\n      }\n    });\n  },\n\n  /**\n   * Assign values to input boxes based on the query string\n   *\n   * @param {URLSearchParams} searchParams - the query string parameters or empty to use the current URL\n   */\n  updateFormInputs(searchParams) {\n    searchParams = searchParams || new URLSearchParams(window.location.search);\n    const backgroundParams = searchParams.getAll(\"background\");\n    // set background-type\n    if (backgroundParams.length > 1) {\n      document.querySelector(\"#background-type-gradient\").checked = true;\n    }\n    // set input field and select values\n    searchParams.forEach((val, key) => {\n      const paramInput = document.querySelector(`[name=\"${key}\"]`);\n      if (paramInput) {\n        // set parameter value\n        paramInput.value = val;\n      } else {\n        // add advanced property\n        document.querySelector(\"details.advanced\").open = true;\n        preview.addProperty(key, searchParams.getAll(key).join(\",\"));\n      }\n    });\n    // set background angle and gradient colors\n    if (backgroundParams.length > 1) {\n      document.querySelector(\"#rotate\").value = backgroundParams[0];\n      document.querySelector(\"#background-color1\").value = backgroundParams[1];\n      document.querySelector(\"#background-color2\").value = backgroundParams[2];\n      preview.checkColor(backgroundParams[1], \"background-color1\");\n      preview.checkColor(backgroundParams[2], \"background-color2\");\n    }\n    // set weekday checkboxes\n    this.updateCheckboxes(searchParams.get(\"exclude_days\"), \".weekdays\");\n    // set show sections checkboxes\n    this.updateCheckboxes(searchParams.get(\"sections\"), \".sections\");\n  },\n};\n\nconst clipboard = {\n  /**\n   * Copy the content of an element to the clipboard\n   * @param {Element} el - the element to copy\n   */\n  copy(el) {\n    // create input box to copy from\n    const input = document.createElement(\"input\");\n    if (el.classList.contains(\"copy-md\")) {\n      input.value = document.querySelector(\".md code\").innerText;\n    } else if (el.classList.contains(\"copy-html\")) {\n      input.value = document.querySelector(\".html code\").innerText;\n    } else if (el.classList.contains(\"copy-json\")) {\n      input.value = document.querySelector(\".json code\").innerText;\n    }\n    document.body.appendChild(input);\n    // select all\n    input.select();\n    input.setSelectionRange(0, 99999);\n    // copy\n    document.execCommand(\"copy\");\n    // remove input box\n    input.parentElement.removeChild(input);\n    // set tooltip text\n    el.title = \"Copied!\";\n  },\n};\n\nconst tooltip = {\n  /**\n   * Reset the tooltip text\n   * @param {Element} el - the element to reset the tooltip for\n   */\n  reset(el) {\n    // remove tooltip text\n    el.removeAttribute(\"title\");\n  },\n};\n\n// when the page loads\nwindow.addEventListener(\n  \"load\",\n  () => {\n    // refresh preview on interactions with the page\n    const refresh = () => preview.update();\n    document.addEventListener(\"keyup\", refresh, false);\n    [...document.querySelectorAll(\"select:not(#properties)\")].forEach((element) => {\n      element.addEventListener(\"change\", refresh, false);\n    });\n    // when the background-type changes, remove the background and replace it\n    const toggleBackgroundType = () => {\n      const value = document.querySelector(\"input#background, input#background-color1\")?.value;\n      preview.removeProperty(\"background\");\n      if (value && document.querySelector(\"#background-type-gradient\").checked) {\n        preview.addProperty(\"background\", `45,${value},${value}`);\n      } else if (value) {\n        preview.addProperty(\"background\", value);\n      }\n    };\n    document.querySelector(\"#background-type-solid\").addEventListener(\"change\", toggleBackgroundType, false);\n    document.querySelector(\"#background-type-gradient\").addEventListener(\"change\", toggleBackgroundType, false);\n    // function to update the hidden input box when checkboxes are clicked\n    const updateCheckboxTextField = (parentSelector, inputSelector) => {\n      const checked = document.querySelectorAll(`${parentSelector} input:checked`);\n      document.querySelector(inputSelector).value = [...checked].map((node) => node.value).join(\",\");\n      preview.update();\n    };\n    // when weekdays are toggled, update the input field\n    document.querySelectorAll(\".weekdays input[type='checkbox']\").forEach((el) => {\n      el.addEventListener(\"click\", () => {\n        updateCheckboxTextField(\".weekdays\", \"#exclude-days\");\n      });\n    });\n    // when sections are toggled, update the input field\n    document.querySelectorAll(\".sections input[type='checkbox']\").forEach((el) => {\n      el.addEventListener(\"click\", () => {\n        updateCheckboxTextField(\".sections\", \"#sections\");\n      });\n    });\n    // when mode is set to \"weekly\", disable checkboxes, otherwise enable them\n    const toggleExcludedDaysCheckboxes = () => {\n      const mode = document.querySelector(\"#mode\").value;\n      document.querySelectorAll(\".weekdays input[type='checkbox']\").forEach((el) => {\n        const labelEl = el.nextElementSibling;\n        if (mode === \"weekly\") {\n          el.disabled = true;\n          labelEl.title = \"Disabled in weekly mode\";\n        } else {\n          el.disabled = false;\n          labelEl.title = labelEl.dataset.tooltip;\n        }\n      });\n    };\n    document.querySelector(\"#mode\").addEventListener(\"change\", toggleExcludedDaysCheckboxes, false);\n    // set input boxes to match URL parameters\n    preview.updateFormInputs();\n    toggleExcludedDaysCheckboxes();\n    // update previews\n    preview.update();\n  },\n  false,\n);\n"
  },
  {
    "path": "src/demo/js/toggle-dark.js",
    "content": "/**\n * Set a cookie\n * @param {string} cname - cookie name\n * @param {string} cvalue - cookie value\n * @param {number} exdays - number of days to expire\n */\nfunction setCookie(cname, cvalue, exdays) {\n  const d = new Date();\n  d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);\n  const expires = `expires=${d.toUTCString()}`;\n  document.cookie = `${cname}=${cvalue}; ${expires}; path=/`;\n}\n\n/**\n * Get a cookie\n * @param {string} cname - cookie name\n * @returns {string} the cookie's value\n */\nfunction getCookie(name) {\n  const dc = document.cookie;\n  const prefix = `${name}=`;\n  let begin = dc.indexOf(`; ${prefix}`);\n  /** @type {Number?} */\n  let end = null;\n  if (begin === -1) {\n    begin = dc.indexOf(prefix);\n    if (begin !== 0) return null;\n  } else {\n    begin += 2;\n    end = document.cookie.indexOf(\";\", begin);\n    if (end === -1) {\n      end = dc.length;\n    }\n  }\n  return decodeURI(dc.substring(begin + prefix.length, end));\n}\n\n/**\n * Turn on dark mode\n */\nfunction darkmode() {\n  document.querySelector(\"#darkmode-icon\").innerText = \"🌞\";\n  setCookie(\"darkmode\", \"on\", 9999);\n  document.body.setAttribute(\"data-theme\", \"dark\");\n}\n\n/**\n * Turn on light mode\n */\nfunction lightmode() {\n  document.querySelector(\"#darkmode-icon\").innerText = \"🌙\";\n  setCookie(\"darkmode\", \"off\", 9999);\n  document.body.removeAttribute(\"data-theme\");\n}\n\n/**\n * Toggle theme between light and dark\n */\nfunction toggleTheme() {\n  if (document.body.getAttribute(\"data-theme\") !== \"dark\") {\n    /* dark mode on */\n    darkmode();\n  } else {\n    /* dark mode off */\n    lightmode();\n  }\n}\n\n// set the theme based on the cookie\nif (getCookie(\"darkmode\") === null && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n  darkmode();\n}\n"
  },
  {
    "path": "src/demo/preview.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nrequire_once \"../card.php\";\nrequire_once \"../stats.php\";\n\n$mode = $_GET[\"mode\"] ?? \"daily\";\n\n// generate demo stats\n$demoStats = [\n    \"mode\" => \"daily\",\n    \"totalContributions\" => 2048,\n    \"firstContribution\" => \"2016-08-10\",\n    \"longestStreak\" => [\n        \"start\" => \"2021-12-19\",\n        \"end\" => \"2022-03-14\",\n        \"length\" => 86,\n    ],\n    \"currentStreak\" => [\n        \"start\" => date(\"Y-m-d\", strtotime(\"-15 days\")),\n        \"end\" => date(\"Y-m-d\"),\n        \"length\" => 16,\n    ],\n    \"excludedDays\" => normalizeDays(explode(\",\", $_GET[\"exclude_days\"] ?? \"\")),\n];\n\nif ($mode == \"weekly\") {\n    $demoStats[\"mode\"] = \"weekly\";\n    $demoStats[\"longestStreak\"] = [\n        \"start\" => \"2021-12-19\",\n        \"end\" => \"2022-03-13\",\n        \"length\" => 13,\n    ];\n    $demoStats[\"currentStreak\"] = [\n        \"start\" => getPreviousSunday(date(\"Y-m-d\", strtotime(\"-15 days\"))),\n        \"end\" => getPreviousSunday(date(\"Y-m-d\")),\n        \"length\" => 3,\n    ];\n    unset($demoStats[\"excludedDays\"]);\n}\n\n// set content type to SVG image\nheader(\"Content-Type: image/svg+xml\");\n\ntry {\n    renderOutput($demoStats);\n} catch (InvalidArgumentException | AssertionError $error) {\n    error_log(\"Error {$error->getCode()}: {$error->getMessage()}\");\n    if ($error->getCode() >= 500) {\n        error_log($error->getTraceAsString());\n    }\n    renderOutput($error->getMessage(), $error->getCode());\n}\n"
  },
  {
    "path": "src/index.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n// load functions\nrequire_once \"../vendor/autoload.php\";\nrequire_once \"stats.php\";\nrequire_once \"card.php\";\nrequire_once \"cache.php\";\n\n// load .env\n$dotenv = \\Dotenv\\Dotenv::createImmutable(dirname(__DIR__, 1));\n$dotenv->safeLoad();\n\n// if environment variables are not loaded, display error\nif (!isset($_SERVER[\"TOKEN\"])) {\n    $message = file_exists(dirname(__DIR__ . \"../.env\", 1))\n        ? \"Missing token in config. Check Contributing.md for details.\"\n        : \".env was not found. Check Contributing.md for details.\";\n    renderOutput($message, 500);\n}\n\n// set cache to refresh once per day (24 hours)\n$cacheSeconds = CACHE_DURATION;\nheader(\"Expires: \" . gmdate(\"D, d M Y H:i:s\", time() + $cacheSeconds) . \" GMT\");\nheader(\"Last-Modified: \" . gmdate(\"D, d M Y H:i:s\") . \" GMT\");\nheader(\"Cache-Control: public, max-age=$cacheSeconds\");\n\n// redirect to demo site if user is not given\nif (!isset($_REQUEST[\"user\"])) {\n    header(\"Location: demo/\");\n    exit();\n}\n\ntry {\n    // get streak stats for user given in query string\n    $user = preg_replace(\"/[^a-zA-Z0-9\\-]/\", \"\", $_REQUEST[\"user\"]);\n    $startingYear = isset($_REQUEST[\"starting_year\"]) ? intval($_REQUEST[\"starting_year\"]) : null;\n    $mode = isset($_GET[\"mode\"]) ? $_GET[\"mode\"] : null;\n    $excludeDaysRaw = $_GET[\"exclude_days\"] ?? \"\";\n\n    // Build cache options based on request parameters\n    $cacheOptions = [\n        \"starting_year\" => $startingYear,\n        \"mode\" => $mode,\n        \"exclude_days\" => $excludeDaysRaw,\n    ];\n\n    // Check if cache is disabled\n    $useCache = !isset($_SERVER[\"DISABLE_CACHE\"]) || strtolower($_SERVER[\"DISABLE_CACHE\"]) !== \"true\";\n\n    // Check for cached stats first (24 hour cache) unless cache is disabled\n    $cachedStats = $useCache ? getCachedStats($user, $cacheOptions) : null;\n\n    if ($cachedStats !== null) {\n        // Use cached stats - instant response!\n        $stats = $cachedStats;\n    } else {\n        // Fetch fresh data from GitHub API\n        $contributionGraphs = getContributionGraphs($user, $startingYear);\n        $contributions = getContributionDates($contributionGraphs);\n\n        if ($mode === \"weekly\") {\n            $stats = getWeeklyContributionStats($contributions);\n        } else {\n            // split and normalize excluded days\n            $excludeDays = normalizeDays(explode(\",\", $excludeDaysRaw));\n            $stats = getContributionStats($contributions, $excludeDays);\n        }\n\n        // Cache the stats for 24 hours unless cache is disabled\n        if ($useCache) {\n            setCachedStats($user, $cacheOptions, $stats);\n        }\n    }\n\n    renderOutput($stats);\n} catch (InvalidArgumentException | AssertionError $error) {\n    error_log(\"Error {$error->getCode()}: {$error->getMessage()}\");\n    if ($error->getCode() >= 500) {\n        error_log($error->getTraceAsString());\n    }\n    renderOutput($error->getMessage(), $error->getCode());\n}\n"
  },
  {
    "path": "src/stats.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nrequire_once \"whitelist.php\";\n\n/**\n * Build a GraphQL query for a contribution graph\n *\n * @param string $user GitHub username to get graphs for\n * @param int $year Year to get graph for\n * @return string GraphQL query\n */\nfunction buildContributionGraphQuery(string $user, int $year): string\n{\n    $start = \"$year-01-01T00:00:00Z\";\n    $end = \"$year-12-31T23:59:59Z\";\n    return \"query {\n        user(login: \\\"$user\\\") {\n            createdAt\n            contributionsCollection(from: \\\"$start\\\", to: \\\"$end\\\") {\n                contributionYears\n                contributionCalendar {\n                    weeks {\n                        contributionDays {\n                            contributionCount\n                            date\n                        }\n                    }\n                }\n            }\n        }\n    }\";\n}\n\n/**\n * Execute multiple requests with cURL and handle GitHub API rate limits and errors\n *\n * @param string $user GitHub username to get graphs for\n * @param array<int> $years Years to get graphs for\n * @return array<int,stdClass> List of GraphQL response objects with years as keys\n */\nfunction executeContributionGraphRequests(string $user, array $years): array\n{\n    $tokens = [];\n    $requests = [];\n    // build handles for each year\n    foreach ($years as $year) {\n        $tokens[$year] = getGitHubToken();\n        $query = buildContributionGraphQuery($user, $year);\n        $requests[$year] = getGraphQLCurlHandle($query, $tokens[$year]);\n    }\n    // build multi-curl handle\n    $multi = curl_multi_init();\n    foreach ($requests as $handle) {\n        curl_multi_add_handle($multi, $handle);\n    }\n    // execute queries\n    $running = null;\n    do {\n        curl_multi_exec($multi, $running);\n    } while ($running);\n    // collect responses\n    $responses = [];\n    foreach ($requests as $year => $handle) {\n        $contents = curl_multi_getcontent($handle);\n        $decoded = is_string($contents) ? json_decode($contents) : null;\n        // if response is empty or invalid, retry request one time or throw an error\n        if (empty($decoded) || empty($decoded->data) || !empty($decoded->errors)) {\n            $message = $decoded->errors[0]->message ?? ($decoded->message ?? \"An API error occurred.\");\n            $error_type = $decoded->errors[0]->type ?? \"\";\n            // Missing SSL certificate\n            if (curl_errno($handle) === 60) {\n                throw new AssertionError(\"You don't have a valid SSL Certificate installed or XAMPP.\", 500);\n            }\n            // Other cURL error\n            elseif (curl_errno($handle)) {\n                throw new AssertionError(\"cURL error: \" . curl_error($handle), 500);\n            }\n            // GitHub API error - Not Found\n            elseif ($error_type === \"NOT_FOUND\") {\n                throw new InvalidArgumentException(\"Could not find a user with that name.\", 404);\n            }\n            // if rate limit is exceeded, don't retry with same token\n            if (str_contains($message, \"rate limit exceeded\")) {\n                removeGitHubToken($tokens[$year]);\n            }\n            error_log(\"First attempt to decode response for $user's $year contributions failed. $message\");\n            error_log(\"Contents: $contents\");\n            // retry request\n            $query = buildContributionGraphQuery($user, $year);\n            $token = getGitHubToken();\n            $request = getGraphQLCurlHandle($query, $token);\n            $contents = curl_exec($request);\n            $decoded = is_string($contents) ? json_decode($contents) : null;\n            // if the response is still empty or invalid, log an error and skip the year\n            if (empty($decoded) || empty($decoded->data)) {\n                $message = $decoded->errors[0]->message ?? ($decoded->message ?? \"An API error occurred.\");\n                if (str_contains($message, \"rate limit exceeded\")) {\n                    removeGitHubToken($token);\n                }\n                error_log(\"Failed to decode response for $user's $year contributions after 2 attempts. $message\");\n                error_log(\"Contents: $contents\");\n                continue;\n            }\n        }\n        $responses[$year] = $decoded;\n    }\n    // close the handles\n    foreach ($requests as $request) {\n        curl_multi_remove_handle($multi, $handle);\n    }\n    curl_multi_close($multi);\n    return $responses;\n}\n\n/**\n * Get all HTTP request responses for user's contributions\n *\n * @param string $user GitHub username to get graphs for\n * @param int|null $startingYear Override the minimum year to get graphs for\n * @return array<stdClass> List of contribution graph response objects\n */\nfunction getContributionGraphs(string $user, ?int $startingYear = null): array\n{\n    if (!isWhitelisted($user)) {\n        throw new InvalidArgumentException(\"User not in whitelist.\", 403);\n    }\n\n    // get the list of years the user has contributed and the current year's contribution graph\n    $currentYear = intval(date(\"Y\"));\n    $responses = executeContributionGraphRequests($user, [$currentYear]);\n    // get user's created date (YYYY-MM-DDTHH:MM:SSZ format)\n    $userCreatedDateTimeString = $responses[$currentYear]->data->user->createdAt ?? null;\n    // if there are no contribution years, an API error must have occurred\n    if (empty($userCreatedDateTimeString)) {\n        throw new AssertionError(\"Failed to retrieve contributions. This is likely a GitHub API issue.\", 500);\n    }\n    // extract the year from the created datetime string\n    $userCreatedYear = intval(explode(\"-\", $userCreatedDateTimeString)[0]);\n    // if override parameter is null then define starting year\n    // as the user created year; else use the provided override year\n    $minimumYear = $startingYear ?: $userCreatedYear;\n    // make sure the minimum year is not before 2005 (the year Git was created)\n    $minimumYear = max($minimumYear, 2005);\n    // create an array of years from the user's created year to one year before the current year\n    $yearsToRequest = range($minimumYear, $currentYear - 1);\n    // also check the first contribution year if the year is before 2005 (the year Git was created)\n    // since the user may have backdated some commits to a specific year such as 1970 (see #448)\n    $contributionYears = $responses[$currentYear]->data->user->contributionsCollection->contributionYears ?? [];\n    $firstContributionYear = $contributionYears[count($contributionYears) - 1] ?? $userCreatedYear;\n    if ($firstContributionYear < 2005) {\n        array_unshift($yearsToRequest, $firstContributionYear);\n    }\n    // get the contribution graphs for the previous years\n    $responses += executeContributionGraphRequests($user, $yearsToRequest);\n    return $responses;\n}\n\n/**\n * Get all tokens from environment variables (TOKEN, TOKEN2, TOKEN3, etc.) if they are set\n *\n * @return array<string> List of tokens\n */\nfunction getGitHubTokens(): array\n{\n    // result is already calculated\n    if (isset($GLOBALS[\"ALL_TOKENS\"])) {\n        return $GLOBALS[\"ALL_TOKENS\"];\n    }\n    // find all tokens in environment variables\n    $tokens = isset($_SERVER[\"TOKEN\"]) ? [$_SERVER[\"TOKEN\"]] : [];\n    $index = 2;\n    while (isset($_SERVER[\"TOKEN{$index}\"])) {\n        // add token to list\n        $tokens[] = $_SERVER[\"TOKEN{$index}\"];\n        $index++;\n    }\n    // store for future use\n    $GLOBALS[\"ALL_TOKENS\"] = $tokens;\n    return $tokens;\n}\n\n/**\n * Get a token from the token pool\n *\n * @return string GitHub token\n *\n * @throws AssertionError if no tokens are available\n */\nfunction getGitHubToken(): string\n{\n    $all_tokens = getGitHubTokens();\n    // if there is no available token, throw an error (this should never happen)\n    if (empty($all_tokens)) {\n        throw new AssertionError(\"There is no GitHub token available.\", 500);\n    }\n    return $all_tokens[array_rand($all_tokens)];\n}\n\n/**\n * Remove a token from the token pool\n *\n * @param string $token Token to remove\n * @return void\n *\n * @throws AssertionError if no tokens are available after removing the token\n */\nfunction removeGitHubToken(string $token): void\n{\n    $index = array_search($token, $GLOBALS[\"ALL_TOKENS\"]);\n    if ($index !== false) {\n        unset($GLOBALS[\"ALL_TOKENS\"][$index]);\n    }\n    // if there is no available token, throw an error\n    if (empty($GLOBALS[\"ALL_TOKENS\"])) {\n        throw new AssertionError(\n            \"We are being rate-limited! Check <a href='https://git.io/streak-ratelimit' font-weight='bold'>git.io/streak-ratelimit</a> for details.\",\n            429,\n        );\n    }\n}\n\n/** Create a CurlHandle for a POST request to GitHub's GraphQL API\n *\n * @param string $query GraphQL query\n * @param string $token GitHub token to use for the request\n * @return CurlHandle The curl handle for the request\n */\nfunction getGraphQLCurlHandle(string $query, string $token): CurlHandle\n{\n    $headers = [\n        \"Authorization: bearer $token\",\n        \"Content-Type: application/json\",\n        \"Accept: application/vnd.github.v4.idl\",\n        \"User-Agent: GitHub-Readme-Streak-Stats\",\n    ];\n    $body = [\"query\" => $query];\n    // create curl request\n    $ch = curl_init();\n    curl_setopt($ch, CURLOPT_URL, \"https://api.github.com/graphql\");\n    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\n    curl_setopt($ch, CURLOPT_POST, true);\n    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);\n    curl_setopt($ch, CURLOPT_VERBOSE, false);\n    return $ch;\n}\n\n/**\n * Get an array of all dates with the number of contributions\n *\n * @param array<int,stdClass> $contributionCalendars List of GraphQL response objects by year\n * @return array<string,int> Y-M-D dates mapped to the number of contributions\n */\nfunction getContributionDates(array $contributionGraphs): array\n{\n    $contributions = [];\n    $today = date(\"Y-m-d\");\n    $tomorrow = date(\"Y-m-d\", strtotime(\"tomorrow\"));\n    // sort contribution calendars by year key\n    ksort($contributionGraphs);\n    foreach ($contributionGraphs as $graph) {\n        $weeks = $graph->data->user->contributionsCollection->contributionCalendar->weeks;\n        foreach ($weeks as $week) {\n            foreach ($week->contributionDays as $day) {\n                $date = $day->date;\n                $count = $day->contributionCount;\n                // count contributions up until today\n                // also count next day if user contributed already\n                if ($date <= $today || ($date == $tomorrow && $count > 0)) {\n                    // add contributions to the array\n                    $contributions[$date] = $count;\n                }\n            }\n        }\n    }\n    return $contributions;\n}\n\n/**\n * Normalize names of days of the week (eg. [\"Sunday\", \" mon\", \"TUE\"] -> [\"Sun\", \"Mon\", \"Tue\"])\n *\n * @param array<string> $days List of days of the week\n * @return array<string> List of normalized days of the week\n */\nfunction normalizeDays(array $days): array\n{\n    return array_filter(\n        array_map(function ($dayOfWeek) {\n            // trim whitespace, capitalize first letter only, return first 3 characters\n            $dayOfWeek = substr(ucfirst(strtolower(trim($dayOfWeek))), 0, 3);\n            // return day if valid, otherwise return null\n            return in_array($dayOfWeek, [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"]) ? $dayOfWeek : null;\n        }, $days),\n    );\n}\n\n/**\n * Check if a day is an excluded day of the week\n *\n * @param string $date Date to check (Y-m-d)\n * @param array<string> $excludedDays List of days of the week to exclude\n * @return bool True if the day is excluded, false otherwise\n */\nfunction isExcludedDay(string $date, array $excludedDays): bool\n{\n    if (empty($excludedDays)) {\n        return false;\n    }\n    $day = date(\"D\", strtotime($date)); // \"D\" = Mon, Tue, Wed, etc.\n    return in_array($day, $excludedDays);\n}\n\n/**\n * Get a stats array with the contribution count, daily streak, and dates\n *\n * @param array<string,int> $contributions Y-M-D contribution dates with contribution counts\n * @param array<string> $excludedDays List of days of the week to exclude\n * @return array<string,mixed> Streak stats\n */\nfunction getContributionStats(array $contributions, array $excludedDays = []): array\n{\n    // if no contributions, display error\n    if (empty($contributions)) {\n        throw new AssertionError(\"No contributions found.\", 204);\n    }\n    $today = array_key_last($contributions);\n    $first = array_key_first($contributions);\n    $stats = [\n        \"mode\" => \"daily\",\n        \"totalContributions\" => 0,\n        \"firstContribution\" => \"\",\n        \"longestStreak\" => [\n            \"start\" => $first,\n            \"end\" => $first,\n            \"length\" => 0,\n        ],\n        \"currentStreak\" => [\n            \"start\" => $first,\n            \"end\" => $first,\n            \"length\" => 0,\n        ],\n        \"excludedDays\" => $excludedDays,\n    ];\n\n    // calculate the stats from the contributions array\n    foreach ($contributions as $date => $count) {\n        // add contribution count to total\n        $stats[\"totalContributions\"] += $count;\n        // check if still in streak\n        if ($count > 0 || ($stats[\"currentStreak\"][\"length\"] > 0 && isExcludedDay($date, $excludedDays))) {\n            // increment streak\n            ++$stats[\"currentStreak\"][\"length\"];\n            $stats[\"currentStreak\"][\"end\"] = $date;\n            // set start on first day of streak\n            if ($stats[\"currentStreak\"][\"length\"] == 1) {\n                $stats[\"currentStreak\"][\"start\"] = $date;\n            }\n            // set first contribution date the first time\n            if (!$stats[\"firstContribution\"]) {\n                $stats[\"firstContribution\"] = $date;\n            }\n            // update longestStreak\n            if ($stats[\"currentStreak\"][\"length\"] > $stats[\"longestStreak\"][\"length\"]) {\n                // copy current streak start, end, and length into longest streak\n                $stats[\"longestStreak\"][\"start\"] = $stats[\"currentStreak\"][\"start\"];\n                $stats[\"longestStreak\"][\"end\"] = $stats[\"currentStreak\"][\"end\"];\n                $stats[\"longestStreak\"][\"length\"] = $stats[\"currentStreak\"][\"length\"];\n            }\n        }\n        // reset streak but give exception for today\n        elseif ($date != $today) {\n            // reset streak\n            $stats[\"currentStreak\"][\"length\"] = 0;\n            $stats[\"currentStreak\"][\"start\"] = $today;\n            $stats[\"currentStreak\"][\"end\"] = $today;\n        }\n    }\n    return $stats;\n}\n\n/**\n * Get the previous Sunday of a given date\n *\n * @param string $date Date to get previous Sunday of (Y-m-d)\n * @return string Previous Sunday\n */\nfunction getPreviousSunday(string $date): string\n{\n    $dayOfWeek = date(\"w\", strtotime($date));\n    return date(\"Y-m-d\", strtotime(\"-$dayOfWeek days\", strtotime($date)));\n}\n\n/**\n * Get a stats array with the contribution count, weekly streak, and dates\n *\n * @param array<string,int> $contributions Y-M-D contribution dates with contribution counts\n * @return array<string,mixed> Streak stats\n */\nfunction getWeeklyContributionStats(array $contributions): array\n{\n    // if no contributions, display error\n    if (empty($contributions)) {\n        throw new AssertionError(\"No contributions found.\", 204);\n    }\n    $thisWeek = getPreviousSunday(array_key_last($contributions));\n    $first = array_key_first($contributions);\n    $firstWeek = getPreviousSunday($first);\n    $stats = [\n        \"mode\" => \"weekly\",\n        \"totalContributions\" => 0,\n        \"firstContribution\" => \"\",\n        \"longestStreak\" => [\n            \"start\" => $firstWeek,\n            \"end\" => $firstWeek,\n            \"length\" => 0,\n        ],\n        \"currentStreak\" => [\n            \"start\" => $firstWeek,\n            \"end\" => $firstWeek,\n            \"length\" => 0,\n        ],\n    ];\n\n    // calculate contributions per week\n    $weeks = [];\n    foreach ($contributions as $date => $count) {\n        $week = getPreviousSunday($date);\n        if (!isset($weeks[$week])) {\n            $weeks[$week] = 0;\n        }\n        if ($count > 0) {\n            $weeks[$week] += $count;\n            // set first contribution date the first time\n            if (!$stats[\"firstContribution\"]) {\n                $stats[\"firstContribution\"] = $date;\n            }\n        }\n    }\n\n    // calculate the stats from the contributions array\n    foreach ($weeks as $week => $count) {\n        // add contribution count to total\n        $stats[\"totalContributions\"] += $count;\n        // check if still in streak\n        if ($count > 0) {\n            // increment streak\n            ++$stats[\"currentStreak\"][\"length\"];\n            $stats[\"currentStreak\"][\"end\"] = $week;\n            // set start on first week of streak\n            if ($stats[\"currentStreak\"][\"length\"] == 1) {\n                $stats[\"currentStreak\"][\"start\"] = $week;\n            }\n            // update longestStreak\n            if ($stats[\"currentStreak\"][\"length\"] > $stats[\"longestStreak\"][\"length\"]) {\n                // copy current streak start, end, and length into longest streak\n                $stats[\"longestStreak\"][\"start\"] = $stats[\"currentStreak\"][\"start\"];\n                $stats[\"longestStreak\"][\"end\"] = $stats[\"currentStreak\"][\"end\"];\n                $stats[\"longestStreak\"][\"length\"] = $stats[\"currentStreak\"][\"length\"];\n            }\n        }\n        // reset streak but give exception for this week\n        elseif ($week != $thisWeek) {\n            // reset streak\n            $stats[\"currentStreak\"][\"length\"] = 0;\n            $stats[\"currentStreak\"][\"start\"] = $thisWeek;\n            $stats[\"currentStreak\"][\"end\"] = $thisWeek;\n        }\n    }\n    return $stats;\n}\n"
  },
  {
    "path": "src/themes.php",
    "content": "<?php\n\n// mapping of theme colors given a theme name\nreturn [\n    \"default\" => [\n        \"background\" => \"#FFFEFE\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FB8C00\",\n        \"fire\" => \"#FB8C00\",\n        \"currStreakNum\" => \"#151515\",\n        \"sideNums\" => \"#151515\",\n        \"currStreakLabel\" => \"#FB8C00\",\n        \"sideLabels\" => \"#151515\",\n        \"dates\" => \"#464646\",\n        \"excludeDaysLabel\" => \"#464646\",\n    ],\n    \"dark\" => [\n        \"background\" => \"#151515\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FB8C00\",\n        \"fire\" => \"#FB8C00\",\n        \"currStreakNum\" => \"#FEFEFE\",\n        \"sideNums\" => \"#FEFEFE\",\n        \"currStreakLabel\" => \"#FB8C00\",\n        \"sideLabels\" => \"#FEFEFE\",\n        \"dates\" => \"#9E9E9E\",\n        \"excludeDaysLabel\" => \"#9E9E9E\",\n    ],\n    \"highcontrast\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#BEBEBE\",\n        \"stroke\" => \"#BEBEBE\",\n        \"ring\" => \"#FB8C00\",\n        \"fire\" => \"#FB8C00\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FB8C00\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#C5C5C5\",\n        \"excludeDaysLabel\" => \"#C5C5C5\",\n    ],\n    \"transparent\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#006AFF\",\n        \"fire\" => \"#006AFF\",\n        \"currStreakNum\" => \"#0579C3\",\n        \"sideNums\" => \"#006AFF\",\n        \"currStreakLabel\" => \"#0579C3\",\n        \"sideLabels\" => \"#006AFF\",\n        \"dates\" => \"#417E87\",\n        \"excludeDaysLabel\" => \"#417E87\",\n    ],\n    \"radical\" => [\n        \"background\" => \"#141321\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FE428E\",\n        \"fire\" => \"#FE428E\",\n        \"currStreakNum\" => \"#F8D847\",\n        \"sideNums\" => \"#FE428E\",\n        \"currStreakLabel\" => \"#F8D847\",\n        \"sideLabels\" => \"#FE428E\",\n        \"dates\" => \"#A9FEF7\",\n        \"excludeDaysLabel\" => \"#A9FEF7\",\n    ],\n    \"merko\" => [\n        \"background\" => \"#0A0F0B\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#ABD200\",\n        \"fire\" => \"#ABD200\",\n        \"currStreakNum\" => \"#B7D364\",\n        \"sideNums\" => \"#ABD200\",\n        \"currStreakLabel\" => \"#B7D364\",\n        \"sideLabels\" => \"#ABD200\",\n        \"dates\" => \"#68B587\",\n        \"excludeDaysLabel\" => \"#68B587\",\n    ],\n    \"gruvbox\" => [\n        \"background\" => \"#282828\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FABD2F\",\n        \"fire\" => \"#FABD2F\",\n        \"currStreakNum\" => \"#FE8019\",\n        \"sideNums\" => \"#FABD2F\",\n        \"currStreakLabel\" => \"#FE8019\",\n        \"sideLabels\" => \"#FABD2F\",\n        \"dates\" => \"#8EC07C\",\n        \"excludeDaysLabel\" => \"#8EC07C\",\n    ],\n    \"gruvbox-duo\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#FABD2F\",\n        \"fire\" => \"#FABD2F\",\n        \"currStreakNum\" => \"#FE8019\",\n        \"sideNums\" => \"#FABD2F\",\n        \"currStreakLabel\" => \"#FE8019\",\n        \"sideLabels\" => \"#FABD2F\",\n        \"dates\" => \"#8EC07C\",\n        \"excludeDaysLabel\" => \"#8EC07C\",\n    ],\n    \"tokyonight\" => [\n        \"background\" => \"#1A1B27\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#70A5FD\",\n        \"fire\" => \"#70A5FD\",\n        \"currStreakNum\" => \"#BF91F3\",\n        \"sideNums\" => \"#70A5FD\",\n        \"currStreakLabel\" => \"#BF91F3\",\n        \"sideLabels\" => \"#70A5FD\",\n        \"dates\" => \"#38BDAE\",\n        \"excludeDaysLabel\" => \"#38BDAE\",\n    ],\n    \"tokyonight-duo\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#70A5FD\",\n        \"fire\" => \"#70A5FD\",\n        \"currStreakNum\" => \"#BF91F3\",\n        \"sideNums\" => \"#70A5FD\",\n        \"currStreakLabel\" => \"#BF91F3\",\n        \"sideLabels\" => \"#70A5FD\",\n        \"dates\" => \"#38BDAE\",\n        \"excludeDaysLabel\" => \"#38BDAE\",\n    ],\n    \"onedark\" => [\n        \"background\" => \"#282C34\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E4BF7A\",\n        \"fire\" => \"#E4BF7A\",\n        \"currStreakNum\" => \"#8EB573\",\n        \"sideNums\" => \"#E4BF7A\",\n        \"currStreakLabel\" => \"#8EB573\",\n        \"sideLabels\" => \"#E4BF7A\",\n        \"dates\" => \"#DF6D74\",\n        \"excludeDaysLabel\" => \"#DF6D74\",\n    ],\n    \"onedark-duo\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#E4BF7A\",\n        \"fire\" => \"#E4BF7A\",\n        \"currStreakNum\" => \"#8EB573\",\n        \"sideNums\" => \"#E4BF7A\",\n        \"currStreakLabel\" => \"#8EB573\",\n        \"sideLabels\" => \"#E4BF7A\",\n        \"dates\" => \"#DF6D74\",\n        \"excludeDaysLabel\" => \"#DF6D74\",\n    ],\n    \"cobalt\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E683D9\",\n        \"fire\" => \"#E683D9\",\n        \"currStreakNum\" => \"#0480EF\",\n        \"sideNums\" => \"#E683D9\",\n        \"currStreakLabel\" => \"#0480EF\",\n        \"sideLabels\" => \"#E683D9\",\n        \"dates\" => \"#75EEB2\",\n        \"excludeDaysLabel\" => \"#75EEB2\",\n    ],\n    \"synthwave\" => [\n        \"background\" => \"#2B213A\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E2E9EC\",\n        \"fire\" => \"#E2E9EC\",\n        \"currStreakNum\" => \"#EF8539\",\n        \"sideNums\" => \"#E2E9EC\",\n        \"currStreakLabel\" => \"#EF8539\",\n        \"sideLabels\" => \"#E2E9EC\",\n        \"dates\" => \"#E5289E\",\n        \"excludeDaysLabel\" => \"#E5289E\",\n    ],\n    \"dracula\" => [\n        \"background\" => \"#282A36\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF6E96\",\n        \"fire\" => \"#FF6E96\",\n        \"currStreakNum\" => \"#79DAFA\",\n        \"sideNums\" => \"#FF6E96\",\n        \"currStreakLabel\" => \"#79DAFA\",\n        \"sideLabels\" => \"#FF6E96\",\n        \"dates\" => \"#F8F8F2\",\n        \"excludeDaysLabel\" => \"#F8F8F2\",\n    ],\n    \"prussian\" => [\n        \"background\" => \"#172F45\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#BDDFFF\",\n        \"fire\" => \"#BDDFFF\",\n        \"currStreakNum\" => \"#38A0FF\",\n        \"sideNums\" => \"#BDDFFF\",\n        \"currStreakLabel\" => \"#38A0FF\",\n        \"sideLabels\" => \"#BDDFFF\",\n        \"dates\" => \"#6E93B5\",\n        \"excludeDaysLabel\" => \"#6E93B5\",\n    ],\n    \"monokai\" => [\n        \"background\" => \"#272822\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#EB1F6A\",\n        \"fire\" => \"#EB1F6A\",\n        \"currStreakNum\" => \"#E28905\",\n        \"sideNums\" => \"#EB1F6A\",\n        \"currStreakLabel\" => \"#E28905\",\n        \"sideLabels\" => \"#EB1F6A\",\n        \"dates\" => \"#F1F1EB\",\n        \"excludeDaysLabel\" => \"#F1F1EB\",\n    ],\n    \"vue\" => [\n        \"background\" => \"#FFFEFE\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#41B883\",\n        \"fire\" => \"#41B883\",\n        \"currStreakNum\" => \"#41B883\",\n        \"sideNums\" => \"#41B883\",\n        \"currStreakLabel\" => \"#41B883\",\n        \"sideLabels\" => \"#41B883\",\n        \"dates\" => \"#273849\",\n        \"excludeDaysLabel\" => \"#273849\",\n    ],\n    \"vue-dark\" => [\n        \"background\" => \"#273849\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#41B883\",\n        \"fire\" => \"#41B883\",\n        \"currStreakNum\" => \"#41B883\",\n        \"sideNums\" => \"#41B883\",\n        \"currStreakLabel\" => \"#41B883\",\n        \"sideLabels\" => \"#41B883\",\n        \"dates\" => \"#FFFEFE\",\n        \"excludeDaysLabel\" => \"#FFFEFE\",\n    ],\n    \"shades-of-purple\" => [\n        \"background\" => \"#2D2B55\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FAD000\",\n        \"fire\" => \"#FAD000\",\n        \"currStreakNum\" => \"#B362FF\",\n        \"sideNums\" => \"#FAD000\",\n        \"currStreakLabel\" => \"#B362FF\",\n        \"sideLabels\" => \"#FAD000\",\n        \"dates\" => \"#A599E9\",\n        \"excludeDaysLabel\" => \"#A599E9\",\n    ],\n    \"nightowl\" => [\n        \"background\" => \"#011627\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#C792EA\",\n        \"fire\" => \"#C792EA\",\n        \"currStreakNum\" => \"#FFEB95\",\n        \"sideNums\" => \"#C792EA\",\n        \"currStreakLabel\" => \"#FFEB95\",\n        \"sideLabels\" => \"#C792EA\",\n        \"dates\" => \"#7FDBCA\",\n        \"excludeDaysLabel\" => \"#7FDBCA\",\n    ],\n    \"buefy\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#7957D5\",\n        \"fire\" => \"#7957D5\",\n        \"currStreakNum\" => \"#FF3860\",\n        \"sideNums\" => \"#7957D5\",\n        \"currStreakLabel\" => \"#FF3860\",\n        \"sideLabels\" => \"#7957D5\",\n        \"dates\" => \"#363636\",\n        \"excludeDaysLabel\" => \"#363636\",\n    ],\n    \"buefy-dark\" => [\n        \"background\" => \"#1A1B27\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#7957D5\",\n        \"fire\" => \"#7957D5\",\n        \"currStreakNum\" => \"#FF3860\",\n        \"sideNums\" => \"#7957D5\",\n        \"currStreakLabel\" => \"#FF3860\",\n        \"sideLabels\" => \"#7957D5\",\n        \"dates\" => \"#ABABAB\",\n        \"excludeDaysLabel\" => \"#ABABAB\",\n    ],\n    \"blue-green\" => [\n        \"background\" => \"#040F0F\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#2F97C1\",\n        \"fire\" => \"#2F97C1\",\n        \"currStreakNum\" => \"#F5B700\",\n        \"sideNums\" => \"#2F97C1\",\n        \"currStreakLabel\" => \"#F5B700\",\n        \"sideLabels\" => \"#2F97C1\",\n        \"dates\" => \"#0CF574\",\n        \"excludeDaysLabel\" => \"#0CF574\",\n    ],\n    \"algolia\" => [\n        \"background\" => \"#050F2C\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#00AEFF\",\n        \"fire\" => \"#00AEFF\",\n        \"currStreakNum\" => \"#2DDE98\",\n        \"sideNums\" => \"#00AEFF\",\n        \"currStreakLabel\" => \"#2DDE98\",\n        \"sideLabels\" => \"#00AEFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"great-gatsby\" => [\n        \"background\" => \"#000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFA726\",\n        \"fire\" => \"#FFA726\",\n        \"currStreakNum\" => \"#FFB74D\",\n        \"sideNums\" => \"#FFA726\",\n        \"currStreakLabel\" => \"#FFB74D\",\n        \"sideLabels\" => \"#FFA726\",\n        \"dates\" => \"#FFD95B\",\n        \"excludeDaysLabel\" => \"#FFD95B\",\n    ],\n    \"darcula\" => [\n        \"background\" => \"#242424\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#BA5F17\",\n        \"fire\" => \"#BA5F17\",\n        \"currStreakNum\" => \"#84628F\",\n        \"sideNums\" => \"#BA5F17\",\n        \"currStreakLabel\" => \"#84628F\",\n        \"sideLabels\" => \"#BA5F17\",\n        \"dates\" => \"#BEBEBE\",\n        \"excludeDaysLabel\" => \"#BEBEBE\",\n    ],\n    \"bear\" => [\n        \"background\" => \"#1F2023\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E03C8A\",\n        \"fire\" => \"#E03C8A\",\n        \"currStreakNum\" => \"#00AEFF\",\n        \"sideNums\" => \"#E03C8A\",\n        \"currStreakLabel\" => \"#00AEFF\",\n        \"sideLabels\" => \"#E03C8A\",\n        \"dates\" => \"#BCB28D\",\n        \"excludeDaysLabel\" => \"#BCB28D\",\n    ],\n    \"solarized-dark\" => [\n        \"background\" => \"#002B36\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#268BD2\",\n        \"fire\" => \"#268BD2\",\n        \"currStreakNum\" => \"#B58900\",\n        \"sideNums\" => \"#268BD2\",\n        \"currStreakLabel\" => \"#B58900\",\n        \"sideLabels\" => \"#268BD2\",\n        \"dates\" => \"#859900\",\n        \"excludeDaysLabel\" => \"#859900\",\n    ],\n    \"solarized-light\" => [\n        \"background\" => \"#FDF6E3\",\n        \"border\" => \"#ABABAB\",\n        \"stroke\" => \"#ABABAB\",\n        \"ring\" => \"#268BD2\",\n        \"fire\" => \"#268BD2\",\n        \"currStreakNum\" => \"#B58900\",\n        \"sideNums\" => \"#268BD2\",\n        \"currStreakLabel\" => \"#B58900\",\n        \"sideLabels\" => \"#268BD2\",\n        \"dates\" => \"#859900\",\n        \"excludeDaysLabel\" => \"#859900\",\n    ],\n    \"chartreuse-dark\" => [\n        \"background\" => \"#000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#7FFF00\",\n        \"fire\" => \"#7FFF00\",\n        \"currStreakNum\" => \"#00AEFF\",\n        \"sideNums\" => \"#7FFF00\",\n        \"currStreakLabel\" => \"#00AEFF\",\n        \"sideLabels\" => \"#7FFF00\",\n        \"dates\" => \"#FFF\",\n        \"excludeDaysLabel\" => \"#FFF\",\n    ],\n    \"nord\" => [\n        \"background\" => \"#2E3440\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#81A1C1\",\n        \"fire\" => \"#81A1C1\",\n        \"currStreakNum\" => \"#88C0D0\",\n        \"sideNums\" => \"#81A1C1\",\n        \"currStreakLabel\" => \"#88C0D0\",\n        \"sideLabels\" => \"#81A1C1\",\n        \"dates\" => \"#D8DEE9\",\n        \"excludeDaysLabel\" => \"#D8DEE9\",\n    ],\n    \"gotham\" => [\n        \"background\" => \"#0C1014\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#2AA889\",\n        \"fire\" => \"#2AA889\",\n        \"currStreakNum\" => \"#599CAB\",\n        \"sideNums\" => \"#2AA889\",\n        \"currStreakLabel\" => \"#599CAB\",\n        \"sideLabels\" => \"#2AA889\",\n        \"dates\" => \"#99D1CE\",\n        \"excludeDaysLabel\" => \"#99D1CE\",\n    ],\n    \"material\" => [\n        \"background\" => \"#263238\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#80CBC4\",\n        \"fire\" => \"#80CBC4\",\n        \"currStreakNum\" => \"#FFAB91\",\n        \"sideNums\" => \"#80CBC4\",\n        \"currStreakLabel\" => \"#FFAB91\",\n        \"sideLabels\" => \"#80CBC4\",\n        \"dates\" => \"#B0BEC5\",\n        \"excludeDaysLabel\" => \"#B0BEC5\",\n    ],\n    \"material-palenight\" => [\n        \"background\" => \"#292D3E\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#C792EA\",\n        \"fire\" => \"#C792EA\",\n        \"currStreakNum\" => \"#89DDFF\",\n        \"sideNums\" => \"#C792EA\",\n        \"currStreakLabel\" => \"#89DDFF\",\n        \"sideLabels\" => \"#C792EA\",\n        \"dates\" => \"#A6ACCD\",\n        \"excludeDaysLabel\" => \"#A6ACCD\",\n    ],\n    \"graywhite\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#ABABAB\",\n        \"stroke\" => \"#ABABAB\",\n        \"ring\" => \"#24292E\",\n        \"fire\" => \"#24292E\",\n        \"currStreakNum\" => \"#24292E\",\n        \"sideNums\" => \"#24292E\",\n        \"currStreakLabel\" => \"#24292E\",\n        \"sideLabels\" => \"#24292E\",\n        \"dates\" => \"#24292E\",\n        \"excludeDaysLabel\" => \"#24292E\",\n    ],\n    \"vision-friendly-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFB000\",\n        \"fire\" => \"#FFB000\",\n        \"currStreakNum\" => \"#785EF0\",\n        \"sideNums\" => \"#FFB000\",\n        \"currStreakLabel\" => \"#785EF0\",\n        \"sideLabels\" => \"#FFB000\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"ayu-mirage\" => [\n        \"background\" => \"#1F2430\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#F4CD7C\",\n        \"fire\" => \"#F4CD7C\",\n        \"currStreakNum\" => \"#73D0FF\",\n        \"sideNums\" => \"#F4CD7C\",\n        \"currStreakLabel\" => \"#73D0FF\",\n        \"sideLabels\" => \"#F4CD7C\",\n        \"dates\" => \"#C7C8C2\",\n        \"excludeDaysLabel\" => \"#C7C8C2\",\n    ],\n    \"midnight-purple\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#9745F5\",\n        \"fire\" => \"#9745F5\",\n        \"currStreakNum\" => \"#9F4BFF\",\n        \"sideNums\" => \"#9745F5\",\n        \"currStreakLabel\" => \"#9F4BFF\",\n        \"sideLabels\" => \"#9745F5\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"calm\" => [\n        \"background\" => \"#373F51\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E07A5F\",\n        \"fire\" => \"#E07A5F\",\n        \"currStreakNum\" => \"#EDAE49\",\n        \"sideNums\" => \"#E07A5F\",\n        \"currStreakLabel\" => \"#EDAE49\",\n        \"sideLabels\" => \"#E07A5F\",\n        \"dates\" => \"#EBCFB2\",\n        \"excludeDaysLabel\" => \"#EBCFB2\",\n    ],\n    \"flag-india\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF8F1C\",\n        \"fire\" => \"#FF8F1C\",\n        \"currStreakNum\" => \"#250E62\",\n        \"sideNums\" => \"#FF8F1C\",\n        \"currStreakLabel\" => \"#250E62\",\n        \"sideLabels\" => \"#FF8F1C\",\n        \"dates\" => \"#509E2F\",\n        \"excludeDaysLabel\" => \"#509E2F\",\n    ],\n    \"omni\" => [\n        \"background\" => \"#191622\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF79C6\",\n        \"fire\" => \"#FF79C6\",\n        \"currStreakNum\" => \"#E7DE79\",\n        \"sideNums\" => \"#FF79C6\",\n        \"currStreakLabel\" => \"#E7DE79\",\n        \"sideLabels\" => \"#FF79C6\",\n        \"dates\" => \"#E1E1E6\",\n        \"excludeDaysLabel\" => \"#E1E1E6\",\n    ],\n    \"react\" => [\n        \"background\" => \"#20232A\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#61DAFB\",\n        \"fire\" => \"#61DAFB\",\n        \"currStreakNum\" => \"#61DAFB\",\n        \"sideNums\" => \"#61DAFB\",\n        \"currStreakLabel\" => \"#61DAFB\",\n        \"sideLabels\" => \"#61DAFB\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"jolly\" => [\n        \"background\" => \"#291B3E\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF64DA\",\n        \"fire\" => \"#FF64DA\",\n        \"currStreakNum\" => \"#A960FF\",\n        \"sideNums\" => \"#FF64DA\",\n        \"currStreakLabel\" => \"#A960FF\",\n        \"sideLabels\" => \"#FF64DA\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"maroongold\" => [\n        \"background\" => \"#260000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#F7EF8A\",\n        \"fire\" => \"#F7EF8A\",\n        \"currStreakNum\" => \"#F7EF8A\",\n        \"sideNums\" => \"#F7EF8A\",\n        \"currStreakLabel\" => \"#F7EF8A\",\n        \"sideLabels\" => \"#F7EF8A\",\n        \"dates\" => \"#E0AA3E\",\n        \"excludeDaysLabel\" => \"#E0AA3E\",\n    ],\n    \"yeblu\" => [\n        \"background\" => \"#002046\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFFF00\",\n        \"fire\" => \"#FFFF00\",\n        \"currStreakNum\" => \"#FFFF00\",\n        \"sideNums\" => \"#FFFF00\",\n        \"currStreakLabel\" => \"#FFFF00\",\n        \"sideLabels\" => \"#FFFF00\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"blueberry\" => [\n        \"background\" => \"#242938\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#82AAFF\",\n        \"fire\" => \"#82AAFF\",\n        \"currStreakNum\" => \"#89DDFF\",\n        \"sideNums\" => \"#82AAFF\",\n        \"currStreakLabel\" => \"#89DDFF\",\n        \"sideLabels\" => \"#82AAFF\",\n        \"dates\" => \"#27E8A7\",\n        \"excludeDaysLabel\" => \"#27E8A7\",\n    ],\n    \"blueberry-duo\" => [\n        \"background\" => \"#0000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#82AAFF\",\n        \"fire\" => \"#82AAFF\",\n        \"currStreakNum\" => \"#89DDFF\",\n        \"sideNums\" => \"#82AAFF\",\n        \"currStreakLabel\" => \"#89DDFF\",\n        \"sideLabels\" => \"#82AAFF\",\n        \"dates\" => \"#27E8A7\",\n        \"excludeDaysLabel\" => \"#27E8A7\",\n    ],\n    \"slateorange\" => [\n        \"background\" => \"#36393F\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FAA627\",\n        \"fire\" => \"#FAA627\",\n        \"currStreakNum\" => \"#FAA627\",\n        \"sideNums\" => \"#FAA627\",\n        \"currStreakLabel\" => \"#FAA627\",\n        \"sideLabels\" => \"#FAA627\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"kacho-ga\" => [\n        \"background\" => \"#402B23\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#BF4A3F\",\n        \"fire\" => \"#BF4A3F\",\n        \"currStreakNum\" => \"#A64833\",\n        \"sideNums\" => \"#BF4A3F\",\n        \"currStreakLabel\" => \"#A64833\",\n        \"sideLabels\" => \"#BF4A3F\",\n        \"dates\" => \"#D9C8A9\",\n        \"excludeDaysLabel\" => \"#D9C8A9\",\n    ],\n    \"ads-juicy-fresh\" => [\n        \"background\" => \"#0D0C15\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#81FF00\",\n        \"fire\" => \"#81FF00\",\n        \"currStreakNum\" => \"#FFF\",\n        \"sideNums\" => \"#FFF\",\n        \"currStreakLabel\" => \"#FF5700\",\n        \"sideLabels\" => \"#FFF\",\n        \"dates\" => \"#6562AF\",\n        \"excludeDaysLabel\" => \"#6562AF\",\n    ],\n    \"black-ice\" => [\n        \"background\" => \"#151515\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#00E7FF\",\n        \"fire\" => \"#00E7FF\",\n        \"currStreakNum\" => \"#FFF\",\n        \"sideNums\" => \"#FFF\",\n        \"currStreakLabel\" => \"#00E7FF\",\n        \"sideLabels\" => \"#FFF\",\n        \"dates\" => \"#9F9F9F\",\n        \"excludeDaysLabel\" => \"#9F9F9F\",\n    ],\n    \"soft-green\" => [\n        \"background\" => \"#222428\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#00DC4D\",\n        \"fire\" => \"#00DC4D\",\n        \"currStreakNum\" => \"#00DC4D\",\n        \"sideNums\" => \"#3DDC77\",\n        \"currStreakLabel\" => \"#00DC4D\",\n        \"sideLabels\" => \"#3DDC77\",\n        \"dates\" => \"#CECECE\",\n        \"excludeDaysLabel\" => \"#CECECE\",\n    ],\n    \"blood\" => [\n        \"background\" => \"#FFF\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#FF5F5F\",\n        \"fire\" => \"#357291\",\n        \"currStreakNum\" => \"#FF5F5F\",\n        \"sideNums\" => \"#FF5F5F\",\n        \"currStreakLabel\" => \"#FF5F5F\",\n        \"sideLabels\" => \"#FF5F5F\",\n        \"dates\" => \"#273849\",\n        \"excludeDaysLabel\" => \"#273849\",\n    ],\n    \"blood-dark\" => [\n        \"background\" => \"#142B37\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF5F5F\",\n        \"fire\" => \"#357291\",\n        \"currStreakNum\" => \"#FF5F5F\",\n        \"sideNums\" => \"#FF5F5F\",\n        \"currStreakLabel\" => \"#FF5F5F\",\n        \"sideLabels\" => \"#FF5F5F\",\n        \"dates\" => \"#FFF\",\n        \"excludeDaysLabel\" => \"#FFF\",\n    ],\n    \"green-nur\" => [\n        \"background\" => \"#0A1E17\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#5AFFC8\",\n        \"fire\" => \"#5AFFC8\",\n        \"currStreakNum\" => \"#5AFFC8\",\n        \"sideNums\" => \"#5AFFC8\",\n        \"currStreakLabel\" => \"#5AFFC8\",\n        \"sideLabels\" => \"#5AFFC8\",\n        \"dates\" => \"#FFF\",\n        \"excludeDaysLabel\" => \"#FFF\",\n    ],\n    \"neon-dark\" => [\n        \"background\" => \"#020200\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#E41D44\",\n        \"fire\" => \"#E41D44\",\n        \"currStreakNum\" => \"#F9DD3C\",\n        \"currStreakLabel\" => \"#F9DD3C\",\n        \"sideNums\" => \"#5CADC0\",\n        \"sideLabels\" => \"#5CADC0\",\n        \"dates\" => \"#ED7B25\",\n        \"excludeDaysLabel\" => \"#ED7B25\",\n    ],\n    \"neon-palenight\" => [\n        \"background\" => \"#212237\",\n        \"border\" => \"#A8A8A8\",\n        \"stroke\" => \"#A8A8A8\",\n        \"ring\" => \"#E41D44\",\n        \"fire\" => \"#E41D44\",\n        \"currStreakNum\" => \"#F9DD3C\",\n        \"currStreakLabel\" => \"#F9DD3C\",\n        \"sideNums\" => \"#5CADC0\",\n        \"sideLabels\" => \"#5CADC0\",\n        \"dates\" => \"#ED7B25\",\n        \"excludeDaysLabel\" => \"#ED7B25\",\n    ],\n    \"dark-smoky\" => [\n        \"background\" => \"#0B0C10\",\n        \"border\" => \"#C5C6C7\",\n        \"stroke\" => \"#C5C6C7\",\n        \"ring\" => \"#EDF5E1\",\n        \"fire\" => \"#EDF5E1\",\n        \"currStreakNum\" => \"#66FCF1\",\n        \"currStreakLabel\" => \"#66FCF1\",\n        \"sideNums\" => \"#EDF5E1\",\n        \"sideLabels\" => \"#EDF5E1\",\n        \"dates\" => \"#45A29E\",\n        \"excludeDaysLabel\" => \"#45A29E\",\n    ],\n    \"monokai-metallian\" => [\n        \"background\" => \"#1F222E\",\n        \"border\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#F85D7F\",\n        \"currStreakNum\" => \"#F8D866\",\n        \"dates\" => \"#9CA2B8\",\n        \"excludeDaysLabel\" => \"#9CA2B8\",\n        \"fire\" => \"#FC9867\",\n        \"ring\" => \"#FC9867\",\n        \"sideLabels\" => \"#F85D7F\",\n        \"sideNums\" => \"#F8D866\",\n        \"stroke\" => \"#4A4F64\",\n    ],\n    \"city-lights\" => [\n        \"background\" => \"#1D252C\",\n        \"border\" => \"#1D252C\",\n        \"currStreakLabel\" => \"#5D8CB3\",\n        \"currStreakNum\" => \"#5D8CB3\",\n        \"dates\" => \"#5D8CB3\",\n        \"excludeDaysLabel\" => \"#5D8CB3\",\n        \"fire\" => \"#718CA1\",\n        \"ring\" => \"#718CA1\",\n        \"sideLabels\" => \"#5D8CB3\",\n        \"sideNums\" => \"#61788A\",\n        \"stroke\" => \"#536676\",\n    ],\n    \"blux\" => [\n        \"background\" => \"#263D46\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#018596\",\n        \"fire\" => \"#28ECFA\",\n        \"currStreakNum\" => \"#28ECFA\",\n        \"sideNums\" => \"#28ECFA\",\n        \"currStreakLabel\" => \"#28ECFA\",\n        \"sideLabels\" => \"#0F80AA\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"earth\" => [\n        \"background\" => \"#1E1615\",\n        \"border\" => \"#BA9D6F\",\n        \"stroke\" => \"#BA9D6F\",\n        \"ring\" => \"#C48519\",\n        \"fire\" => \"#C48519\",\n        \"currStreakNum\" => \"#639E29\",\n        \"sideNums\" => \"#639E29\",\n        \"currStreakLabel\" => \"#C48519\",\n        \"sideLabels\" => \"#C48519\",\n        \"dates\" => \"#BA9D6F\",\n        \"excludeDaysLabel\" => \"#BA9D6F\",\n    ],\n    \"deepblue\" => [\n        \"background\" => \"#165795\",\n        \"border\" => \"#BA9D6F\",\n        \"stroke\" => \"#38DD69\",\n        \"ring\" => \"#37DD57\",\n        \"fire\" => \"#1CD577\",\n        \"currStreakNum\" => \"#639E29\",\n        \"sideNums\" => \"#131662\",\n        \"currStreakLabel\" => \"#0FDD21\",\n        \"sideLabels\" => \"#1ADD40\",\n        \"dates\" => \"#11E2E7\",\n        \"excludeDaysLabel\" => \"#11E2E7\",\n    ],\n    \"holi-theme\" => [\n        \"background\" => \"#030314\",\n        \"border\" => \"#85A4C0\",\n        \"stroke\" => \"#2A4555\",\n        \"ring\" => \"#D6E7FF\",\n        \"currStreakNum\" => \"#5FABEE\",\n        \"fire\" => \"#D6E7FF\",\n        \"sideNums\" => \"#5FABEE\",\n        \"currStreakLabel\" => \"#D6E7FF\",\n        \"sideLabels\" => \"#D6E7FF\",\n        \"dates\" => \"#85A4C0\",\n        \"excludeDaysLabel\" => \"#85A4C0\",\n    ],\n    \"ayu-light\" => [\n        \"background\" => \"#FAFAFA\",\n        \"border\" => \"#F0F0F0\",\n        \"stroke\" => \"#BAE67E\",\n        \"ring\" => \"#FF9940\",\n        \"currStreakNum\" => \"#F07171\",\n        \"fire\" => \"#FF9940\",\n        \"sideNums\" => \"#55B4D4\",\n        \"currStreakLabel\" => \"#F07171\",\n        \"sideLabels\" => \"#55B4D4\",\n        \"dates\" => \"#575F66\",\n        \"excludeDaysLabel\" => \"#575F66\",\n    ],\n    \"javascript\" => [\n        \"background\" => \"#F7DF1E\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#000000\",\n        \"ring\" => \"#000000\",\n        \"currStreakNum\" => \"#000000\",\n        \"fire\" => \"#000000\",\n        \"sideNums\" => \"#000000\",\n        \"currStreakLabel\" => \"#000000\",\n        \"sideLabels\" => \"#000000\",\n        \"dates\" => \"#000000\",\n        \"excludeDaysLabel\" => \"#000000\",\n    ],\n    \"javascript-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#F7DF1E\",\n        \"stroke\" => \"#F7DF1E\",\n        \"ring\" => \"#F7DF1E\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"fire\" => \"#F7DF1E\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#F7DF1E\",\n        \"sideLabels\" => \"#F7DF1E\",\n        \"dates\" => \"#F7DF1E\",\n        \"excludeDaysLabel\" => \"#F7DF1E\",\n    ],\n    \"noctis-minimus\" => [\n        \"background\" => \"#1B2932\",\n        \"border\" => \"#F0F0F0\",\n        \"stroke\" => \"#C5CDD3\",\n        \"ring\" => \"#D3B692\",\n        \"currStreakNum\" => \"#72B7C0\",\n        \"fire\" => \"#D3B692\",\n        \"sideNums\" => \"#72B7C0\",\n        \"currStreakLabel\" => \"#72B7C0\",\n        \"sideLabels\" => \"#D3B692\",\n        \"dates\" => \"#C5CDD3\",\n        \"excludeDaysLabel\" => \"#C5CDD3\",\n    ],\n    \"github-dark\" => [\n        \"background\" => \"#0D1117\",\n        \"border\" => \"#39D353\",\n        \"stroke\" => \"#39D353\",\n        \"ring\" => \"#39D353\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"fire\" => \"#1F6FEB\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#39D353\",\n        \"excludeDaysLabel\" => \"#39D353\",\n    ],\n    \"github-dark-blue\" => [\n        \"background\" => \"#0D1117\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#1F6FEB\",\n        \"fire\" => \"#58A6FF\",\n        \"currStreakNum\" => \"#FEFEFE\",\n        \"sideNums\" => \"#58A6FF\",\n        \"currStreakLabel\" => \"#FEFEFE\",\n        \"sideLabels\" => \"#FEFEFE\",\n        \"dates\" => \"#9E9E9E\",\n        \"excludeDaysLabel\" => \"#9E9E9E\",\n    ],\n    \"github-light\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#39D353\",\n        \"stroke\" => \"#39D353\",\n        \"ring\" => \"#39D353\",\n        \"currStreakNum\" => \"#39D353\",\n        \"fire\" => \"#39D353\",\n        \"sideNums\" => \"#39D353\",\n        \"currStreakLabel\" => \"#24292F\",\n        \"sideLabels\" => \"#24292F\",\n        \"dates\" => \"#1F6FEB\",\n        \"excludeDaysLabel\" => \"#1F6FEB\",\n    ],\n    \"elegant\" => [\n        \"background\" => \"#03071E\",\n        \"border\" => \"#ABCDEF\",\n        \"stroke\" => \"#ABCDEF\",\n        \"ring\" => \"#CA6702\",\n        \"currStreakNum\" => \"#ABCDEF\",\n        \"fire\" => \"#E85D04\",\n        \"sideNums\" => \"#ABDCFE\",\n        \"currStreakLabel\" => \"#ABCDEF\",\n        \"sideLabels\" => \"#FEDCBA\",\n        \"dates\" => \"#FF7B00\",\n        \"excludeDaysLabel\" => \"#FF7B00\",\n    ],\n    \"leafy\" => [\n        \"background\" => \"#081C15\",\n        \"border\" => \"#ABCDEF\",\n        \"stroke\" => \"#081C15\",\n        \"ring\" => \"#ABCDEF\",\n        \"currStreakNum\" => \"#FF5400\",\n        \"fire\" => \"#ABCDEF\",\n        \"sideNums\" => \"#FF5400\",\n        \"currStreakLabel\" => \"#FF5400\",\n        \"sideLabels\" => \"#ABCABC\",\n        \"dates\" => \"#FECFEC\",\n        \"excludeDaysLabel\" => \"#FECFEC\",\n    ],\n    \"navy-gear\" => [\n        \"background\" => \"#000021\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#1FA0DD\",\n        \"fire\" => \"#1FA0DD\",\n        \"currStreakNum\" => \"#C3DD00\",\n        \"sideNums\" => \"#C3DD00\",\n        \"currStreakLabel\" => \"#C3DD00\",\n        \"sideLabels\" => \"#C3DD00\",\n        \"dates\" => \"#1FA0DD\",\n        \"excludeDaysLabel\" => \"#1FA0DD\",\n    ],\n    \"hacker\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#20C20E\",\n        \"stroke\" => \"#20C20E\",\n        \"ring\" => \"#20C20E\",\n        \"fire\" => \"#20C20E\",\n        \"currStreakNum\" => \"#20C20E\",\n        \"sideNums\" => \"#20C20E\",\n        \"currStreakLabel\" => \"#20C20E\",\n        \"sideLabels\" => \"#20C20E\",\n        \"dates\" => \"#20C20E\",\n        \"excludeDaysLabel\" => \"#20C20E\",\n    ],\n    \"garden\" => [\n        \"background\" => \"#094A4A\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#DDD4A8\",\n        \"ring\" => \"#D2DD3B\",\n        \"fire\" => \"#D2DD3B\",\n        \"currStreakNum\" => \"#D2DD3B\",\n        \"sideNums\" => \"#D2DD3B\",\n        \"currStreakLabel\" => \"#6FDD6C\",\n        \"sideLabels\" => \"#6FDD6C\",\n        \"dates\" => \"#6FDD6C\",\n        \"excludeDaysLabel\" => \"#6FDD6C\",\n    ],\n    \"github-green-purple\" => [\n        \"background\" => \"#000\",\n        \"border\" => \"#000\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#7FFF00\",\n        \"fire\" => \"#7FFF00\",\n        \"currStreakNum\" => \"#800080\",\n        \"sideNums\" => \"#7FFF00\",\n        \"currStreakLabel\" => \"#800080\",\n        \"sideLabels\" => \"#7FFF00\",\n        \"dates\" => \"#FFF\",\n        \"excludeDaysLabel\" => \"#FFF\",\n    ],\n    \"icegray\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#515151\",\n        \"stroke\" => \"#515151\",\n        \"ring\" => \"#686868\",\n        \"fire\" => \"#969696\",\n        \"currStreakNum\" => \"#3C3C3C\",\n        \"sideNums\" => \"#535353\",\n        \"currStreakLabel\" => \"#515151\",\n        \"sideLabels\" => \"#515151\",\n        \"dates\" => \"#636363\",\n        \"excludeDaysLabel\" => \"#636363\",\n    ],\n    \"neon-blurange\" => [\n        \"background\" => \"#030D6B\",\n        \"border\" => \"#C7CCFF\",\n        \"stroke\" => \"#C7CCFF\",\n        \"ring\" => \"#FB750B\",\n        \"fire\" => \"#FB750B\",\n        \"currStreakNum\" => \"#25FB88\",\n        \"sideNums\" => \"#FB750B\",\n        \"currStreakLabel\" => \"#25FB88\",\n        \"sideLabels\" => \"#25FB88\",\n        \"dates\" => \"#C7CCFF\",\n        \"excludeDaysLabel\" => \"#C7CCFF\",\n    ],\n    \"yellowdark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#A5A5A5\",\n        \"ring\" => \"#FFEF00\",\n        \"fire\" => \"#FF8000\",\n        \"currStreakNum\" => \"#FFEF00\",\n        \"sideNums\" => \"#FFEF00\",\n        \"currStreakLabel\" => \"#FFEF00\",\n        \"sideLabels\" => \"#FFEF00\",\n        \"dates\" => \"#A5A5A5\",\n        \"excludeDaysLabel\" => \"#A5A5A5\",\n    ],\n    \"java-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#F89820\",\n        \"stroke\" => \"#F89820\",\n        \"ring\" => \"#F89820\",\n        \"fire\" => \"#F89820\",\n        \"currStreakNum\" => \"#5382A1\",\n        \"sideNums\" => \"#5382A1\",\n        \"currStreakLabel\" => \"#F89820\",\n        \"sideLabels\" => \"#F89820\",\n        \"dates\" => \"#5382A1\",\n        \"excludeDaysLabel\" => \"#5382A1\",\n    ],\n    \"android-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#3DDC84\",\n        \"stroke\" => \"#3DDC84\",\n        \"ring\" => \"#3DDC84\",\n        \"fire\" => \"#3DDC84\",\n        \"currStreakNum\" => \"#3DDC84\",\n        \"sideNums\" => \"#3DDC84\",\n        \"currStreakLabel\" => \"#3DDC84\",\n        \"sideLabels\" => \"#3DDC84\",\n        \"dates\" => \"#3DDC84\",\n        \"excludeDaysLabel\" => \"#3DDC84\",\n    ],\n    \"deuteranopia-friendly-theme\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#E69F00\",\n        \"fire\" => \"#E69F00\",\n        \"currStreakNum\" => \"#56B4E9\",\n        \"sideNums\" => \"#F0E442\",\n        \"currStreakLabel\" => \"#0072B2\",\n        \"sideLabels\" => \"#CC79A7\",\n        \"dates\" => \"#009E73\",\n        \"excludeDaysLabel\" => \"#009E73\",\n    ],\n    \"windows-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#00A4EF\",\n        \"stroke\" => \"#00A4EF\",\n        \"ring\" => \"#00A4EF\",\n        \"fire\" => \"#00A4EF\",\n        \"currStreakNum\" => \"#00A4EF\",\n        \"sideNums\" => \"#00A4EF\",\n        \"currStreakLabel\" => \"#00A4EF\",\n        \"sideLabels\" => \"#00A4EF\",\n        \"dates\" => \"#00A4EF\",\n        \"excludeDaysLabel\" => \"#00A4EF\",\n    ],\n    \"git-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#F05033\",\n        \"stroke\" => \"#F05033\",\n        \"ring\" => \"#F05033\",\n        \"fire\" => \"#F05033\",\n        \"currStreakNum\" => \"#F05033\",\n        \"sideNums\" => \"#F05033\",\n        \"currStreakLabel\" => \"#F05033\",\n        \"sideLabels\" => \"#F05033\",\n        \"dates\" => \"#F05033\",\n        \"excludeDaysLabel\" => \"#F05033\",\n    ],\n    \"python-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#4B8BBE\",\n        \"stroke\" => \"#4B8BBE\",\n        \"ring\" => \"#FFD43B\",\n        \"fire\" => \"#FFD43B\",\n        \"currStreakNum\" => \"#4B8BBE\",\n        \"sideNums\" => \"#4B8BBE\",\n        \"currStreakLabel\" => \"#FFD43B\",\n        \"sideLabels\" => \"#FFD43B\",\n        \"dates\" => \"#FFD43B\",\n        \"excludeDaysLabel\" => \"#FFD43B\",\n    ],\n    \"sea\" => [\n        \"background\" => \"#1565C0\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"sea-dark\" => [\n        \"background\" => \"#00C0FF\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#00546F\",\n        \"ring\" => \"#000000\",\n        \"fire\" => \"#000000\",\n        \"currStreakNum\" => \"#000000\",\n        \"sideNums\" => \"#000000\",\n        \"currStreakLabel\" => \"#000000\",\n        \"sideLabels\" => \"#000000\",\n        \"dates\" => \"#000000\",\n        \"excludeDaysLabel\" => \"#000000\",\n    ],\n    \"violet-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#FF0089\",\n        \"stroke\" => \"#FF0089\",\n        \"ring\" => \"#FF0089\",\n        \"fire\" => \"#FF0089\",\n        \"currStreakNum\" => \"#FF0089\",\n        \"sideNums\" => \"#FF0089\",\n        \"currStreakLabel\" => \"#FF0089\",\n        \"sideLabels\" => \"#FF0089\",\n        \"dates\" => \"#FF0089\",\n        \"excludeDaysLabel\" => \"#FF0089\",\n    ],\n    \"horizon\" => [\n        \"background\" => \"#1C1E26\",\n        \"border\" => \"#1C1E26\",\n        \"stroke\" => \"#FAB795\",\n        \"ring\" => \"#E95678\",\n        \"fire\" => \"#E95678\",\n        \"currStreakNum\" => \"#59E1E3\",\n        \"sideNums\" => \"#59E1E3\",\n        \"currStreakLabel\" => \"#23BD87\",\n        \"sideLabels\" => \"#23BD87\",\n        \"dates\" => \"#FAB795\",\n        \"excludeDaysLabel\" => \"#FAB795\",\n    ],\n    \"modern-lilac\" => [\n        \"background\" => \"#0A0E12\",\n        \"border\" => \"#1C1E26\",\n        \"stroke\" => \"#1C1E26\",\n        \"ring\" => \"#5D417A\",\n        \"fire\" => \"#5D417A\",\n        \"currStreakNum\" => \"#FAB795\",\n        \"sideNums\" => \"#C770F0\",\n        \"currStreakLabel\" => \"#FAB795\",\n        \"sideLabels\" => \"#C770F0\",\n        \"dates\" => \"#FAB795\",\n        \"excludeDaysLabel\" => \"#FAB795\",\n    ],\n    \"modern-lilac2\" => [\n        \"background\" => \"#0A0E12\",\n        \"border\" => \"#1C1E26\",\n        \"stroke\" => \"#1C1E26\",\n        \"ring\" => \"#5D417A\",\n        \"fire\" => \"#5D417A\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#C770F0\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#C770F0\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"halloween\" => [\n        \"background\" => \"#1C1A2B\",\n        \"border\" => \"#FFC400\",\n        \"stroke\" => \"#FFC400\",\n        \"ring\" => \"#FDEF49\",\n        \"fire\" => \"#FDEF49\",\n        \"currStreakNum\" => \"#FFC400\",\n        \"sideNums\" => \"#FFC400\",\n        \"currStreakLabel\" => \"#FB9600\",\n        \"sideLabels\" => \"#FB9600\",\n        \"dates\" => \"#FFC400\",\n        \"excludeDaysLabel\" => \"#FFC400\",\n    ],\n    \"violet-punch\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#B170FC\",\n        \"ring\" => \"#3934DD\",\n        \"fire\" => \"#D0DDAD\",\n        \"currStreakNum\" => \"#CB0B45\",\n        \"sideNums\" => \"#7B43FF\",\n        \"currStreakLabel\" => \"#AFB5DD\",\n        \"sideLabels\" => \"#AFB5DD\",\n        \"dates\" => \"#DDDDDD\",\n        \"excludeDaysLabel\" => \"#DDDDDD\",\n    ],\n    \"submarine-flowers\" => [\n        \"background\" => \"#013E4E\",\n        \"border\" => \"#013E4E\",\n        \"stroke\" => \"#48FF50\",\n        \"ring\" => \"#FF8888\",\n        \"fire\" => \"#FF8888\",\n        \"currStreakNum\" => \"#FFF000\",\n        \"sideNums\" => \"#FFF000\",\n        \"currStreakLabel\" => \"#FFF000\",\n        \"sideLabels\" => \"#FF8888\",\n        \"dates\" => \"#FF8650\",\n        \"excludeDaysLabel\" => \"#FF8650\",\n    ],\n    \"rising-sun\" => [\n        \"background\" => \"#0C1116\",\n        \"border\" => \"#E4E3E3\",\n        \"stroke\" => \"#E4E3E3\",\n        \"ring\" => \"#F6882B\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"fire\" => \"#F6882B\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFF7ED\",\n        \"sideLabels\" => \"#FFF7ED\",\n        \"dates\" => \"#F6882B\",\n        \"excludeDaysLabel\" => \"#F6882B\",\n    ],\n    \"gruvbox-light\" => [\n        \"background\" => \"#FBF1C7\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#B57614\",\n        \"fire\" => \"#B57614\",\n        \"currStreakNum\" => \"#AF3A03\",\n        \"sideNums\" => \"#B57614\",\n        \"currStreakLabel\" => \"#AF3A03\",\n        \"sideLabels\" => \"#B57614\",\n        \"dates\" => \"#427B58\",\n        \"excludeDaysLabel\" => \"#427B58\",\n    ],\n    \"outrun\" => [\n        \"background\" => \"#141439\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFCC00\",\n        \"fire\" => \"#FFCC00\",\n        \"currStreakNum\" => \"#FF1AFF\",\n        \"sideNums\" => \"#FFCC00\",\n        \"currStreakLabel\" => \"#FF1AFF\",\n        \"sideLabels\" => \"#FFCC00\",\n        \"dates\" => \"#8080FF\",\n        \"excludeDaysLabel\" => \"#8080FF\",\n    ],\n    \"ocean-dark\" => [\n        \"background\" => \"#151A28\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#8957B2\",\n        \"fire\" => \"#8957B2\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#8957B2\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#8957B2\",\n        \"dates\" => \"#92D534\",\n        \"excludeDaysLabel\" => \"#92D534\",\n    ],\n    \"discord-old-blurple\" => [\n        \"background\" => \"#2C2F33\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#7289DA\",\n        \"fire\" => \"#7289DA\",\n        \"currStreakNum\" => \"#7289DA\",\n        \"sideNums\" => \"#7289DA\",\n        \"currStreakLabel\" => \"#7289DA\",\n        \"sideLabels\" => \"#7289DA\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"aura-dark\" => [\n        \"background\" => \"#252334\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF7372\",\n        \"fire\" => \"#FF7372\",\n        \"currStreakNum\" => \"#6CFFD0\",\n        \"sideNums\" => \"#FF7372\",\n        \"currStreakLabel\" => \"#6CFFD0\",\n        \"sideLabels\" => \"#FF7372\",\n        \"dates\" => \"#DBDBDB\",\n        \"excludeDaysLabel\" => \"#DBDBDB\",\n    ],\n    \"panda\" => [\n        \"background\" => \"#31353A\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#19F9D899\",\n        \"fire\" => \"#19F9D899\",\n        \"currStreakNum\" => \"#19F9D899\",\n        \"sideNums\" => \"#19F9D899\",\n        \"currStreakLabel\" => \"#19F9D899\",\n        \"sideLabels\" => \"#19F9D899\",\n        \"dates\" => \"#FF75B5\",\n        \"excludeDaysLabel\" => \"#FF75B5\",\n    ],\n    \"cobalt2\" => [\n        \"background\" => \"#193549\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFC600\",\n        \"fire\" => \"#FFC600\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFC600\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFC600\",\n        \"dates\" => \"#0088FF\",\n        \"excludeDaysLabel\" => \"#0088FF\",\n    ],\n    \"swift\" => [\n        \"background\" => \"#F7F7F7\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#000000\",\n        \"fire\" => \"#000000\",\n        \"currStreakNum\" => \"#F05237\",\n        \"sideNums\" => \"#000000\",\n        \"currStreakLabel\" => \"#F05237\",\n        \"sideLabels\" => \"#000000\",\n        \"dates\" => \"#000000\",\n        \"excludeDaysLabel\" => \"#000000\",\n    ],\n    \"aura\" => [\n        \"background\" => \"#15141B\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#A277FF\",\n        \"fire\" => \"#A277FF\",\n        \"currStreakNum\" => \"#FFCA85\",\n        \"sideNums\" => \"#A277FF\",\n        \"currStreakLabel\" => \"#FFCA85\",\n        \"sideLabels\" => \"#A277FF\",\n        \"dates\" => \"#61FFCA\",\n        \"excludeDaysLabel\" => \"#61FFCA\",\n    ],\n    \"apprentice\" => [\n        \"background\" => \"#262626\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FFFFAF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFAF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#BCBCBC\",\n        \"excludeDaysLabel\" => \"#BCBCBC\",\n    ],\n    \"moltack\" => [\n        \"background\" => \"#F5E1C0\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#86092C\",\n        \"fire\" => \"#86092C\",\n        \"currStreakNum\" => \"#86092C\",\n        \"sideNums\" => \"#86092C\",\n        \"currStreakLabel\" => \"#86092C\",\n        \"sideLabels\" => \"#86092C\",\n        \"dates\" => \"#574038\",\n        \"excludeDaysLabel\" => \"#574038\",\n    ],\n    \"codestackr\" => [\n        \"background\" => \"#09131B\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FF652F\",\n        \"fire\" => \"#FF652F\",\n        \"currStreakNum\" => \"#FFE400\",\n        \"sideNums\" => \"#FF652F\",\n        \"currStreakLabel\" => \"#FFE400\",\n        \"sideLabels\" => \"#FF652F\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"rose-pine\" => [\n        \"background\" => \"#191724\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#9CCFD8\",\n        \"fire\" => \"#9CCFD8\",\n        \"currStreakNum\" => \"#EBBCBA\",\n        \"sideNums\" => \"#9CCFD8\",\n        \"currStreakLabel\" => \"#EBBCBA\",\n        \"sideLabels\" => \"#9CCFD8\",\n        \"dates\" => \"#E0DEF4\",\n        \"excludeDaysLabel\" => \"#E0DEF4\",\n    ],\n    \"date-night\" => [\n        \"background\" => \"#170F0C\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#DA7885\",\n        \"fire\" => \"#DA7885\",\n        \"currStreakNum\" => \"#BB8470\",\n        \"sideNums\" => \"#DA7885\",\n        \"currStreakLabel\" => \"#BB8470\",\n        \"sideLabels\" => \"#DA7885\",\n        \"dates\" => \"#E1B2A2\",\n        \"excludeDaysLabel\" => \"#E1B2A2\",\n    ],\n    \"one-dark-pro\" => [\n        \"background\" => \"#23272E\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#61AFEF\",\n        \"fire\" => \"#61AFEF\",\n        \"currStreakNum\" => \"#C678DD\",\n        \"sideNums\" => \"#61AFEF\",\n        \"currStreakLabel\" => \"#C678DD\",\n        \"sideLabels\" => \"#61AFEF\",\n        \"dates\" => \"#E5C06E\",\n        \"excludeDaysLabel\" => \"#E5C06E\",\n    ],\n    \"rose\" => [\n        \"background\" => \"#E9D8D4\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#8D192B\",\n        \"fire\" => \"#8D192B\",\n        \"currStreakNum\" => \"#B71F36\",\n        \"sideNums\" => \"#8D192B\",\n        \"currStreakLabel\" => \"#B71F36\",\n        \"sideLabels\" => \"#8D192B\",\n        \"dates\" => \"#862931\",\n        \"excludeDaysLabel\" => \"#862931\",\n    ],\n    \"neon\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#00EAD3\",\n        \"fire\" => \"#00EAD3\",\n        \"currStreakNum\" => \"#FF449F\",\n        \"sideNums\" => \"#00EAD3\",\n        \"currStreakLabel\" => \"#FF449F\",\n        \"sideLabels\" => \"#00EAD3\",\n        \"dates\" => \"#FFF5B7\",\n        \"excludeDaysLabel\" => \"#FFF5B7\",\n    ],\n    \"sunset-gradient\" => [\n        \"background\" => \"45,8A2386,E94056,F27120\",\n        \"border\" => \"#850000\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#FB8C00\",\n        \"fire\" => \"#FB8C00\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"ocean-gradient\" => [\n        \"background\" => \"90,0093EA,80D0C8,80D0C8\",\n        \"border\" => \"#000155\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"ambient-gradient\" => [\n        \"background\" => \"35,4158D0,C850C0,FFCC70\",\n        \"border\" => \"#AE58A1\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"catppuccin-latte\" => [\n        \"background\" => \"#EFF1F5\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#179299\",\n        \"fire\" => \"#179299\",\n        \"currStreakNum\" => \"#8839EF\",\n        \"sideNums\" => \"#4C4F69\",\n        \"currStreakLabel\" => \"#8839EF\",\n        \"sideLabels\" => \"#4C4F69\",\n        \"dates\" => \"#5C5F77\",\n        \"excludeDaysLabel\" => \"#5C5F77\",\n    ],\n    \"catppuccin-frappe\" => [\n        \"background\" => \"#303446\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#81C8BE\",\n        \"fire\" => \"#81C8BE\",\n        \"currStreakNum\" => \"#CA9EE6\",\n        \"sideNums\" => \"#C6D0F5\",\n        \"currStreakLabel\" => \"#CA9EE6\",\n        \"sideLabels\" => \"#C6D0F5\",\n        \"dates\" => \"#B5BFE2\",\n        \"excludeDaysLabel\" => \"#B5BFE2\",\n    ],\n    \"catppuccin-macchiato\" => [\n        \"background\" => \"#24273A\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#8BD5CA\",\n        \"fire\" => \"#8BD5CA\",\n        \"currStreakNum\" => \"#C6A0F6\",\n        \"sideNums\" => \"#CAD3F5\",\n        \"currStreakLabel\" => \"#C6A0F6\",\n        \"sideLabels\" => \"#CAD3F5\",\n        \"dates\" => \"#B8C0E0\",\n        \"excludeDaysLabel\" => \"#B8C0E0\",\n    ],\n    \"catppuccin-mocha\" => [\n        \"background\" => \"#1E1E2E\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#94E2D5\",\n        \"fire\" => \"#94E2D5\",\n        \"currStreakNum\" => \"#CBA6F7\",\n        \"sideNums\" => \"#CDD6F4\",\n        \"currStreakLabel\" => \"#CBA6F7\",\n        \"sideLabels\" => \"#CDD6F4\",\n        \"dates\" => \"#BAC2DE\",\n        \"excludeDaysLabel\" => \"#BAC2DE\",\n    ],\n    \"burnt-neon\" => [\n        \"background\" => \"#0D1117\",\n        \"border\" => \"#98989A\",\n        \"stroke\" => \"#98989A\",\n        \"ring\" => \"#FE25B1\",\n        \"fire\" => \"#622B53\",\n        \"currStreakNum\" => \"#FF6906\",\n        \"sideNums\" => \"#01FED1\",\n        \"currStreakLabel\" => \"#01FED1\",\n        \"sideLabels\" => \"#FF6906\",\n        \"dates\" => \"#C6AB07\",\n        \"excludeDaysLabel\" => \"#A5BC0B\",\n    ],\n    \"humoris\" => [\n        \"background\" => \"#DFAF77\",\n        \"border\" => \"#E8E6E4\",\n        \"stroke\" => \"#191919\",\n        \"ring\" => \"#683C2C\",\n        \"fire\" => \"#191419\",\n        \"currStreakNum\" => \"#191419\",\n        \"sideNums\" => \"#191419\",\n        \"currStreakLabel\" => \"#393C3C\",\n        \"sideLabels\" => \"#393C3C\",\n        \"dates\" => \"#444444\",\n        \"excludeDaysLabel\" => \"#444444\",\n    ],\n    \"shadow-red\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#4F0000\",\n        \"stroke\" => \"#4F0000\",\n        \"ring\" => \"#4F0000\",\n        \"fire\" => \"#9A0000\",\n        \"currStreakNum\" => \"#B94242\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#9A0000\",\n        \"sideLabels\" => \"#9A0000\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#B94242\",\n    ],\n    \"shadow-green\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#003D00\",\n        \"stroke\" => \"#003D00\",\n        \"ring\" => \"#003D00\",\n        \"fire\" => \"#007A00\",\n        \"currStreakNum\" => \"#4DB942\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#007A00\",\n        \"sideLabels\" => \"#007A00\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#4DB942\",\n    ],\n    \"shadow-blue\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#004490\",\n        \"stroke\" => \"#004450\",\n        \"ring\" => \"#004450\",\n        \"fire\" => \"#00779A\",\n        \"currStreakNum\" => \"#3E6BFF\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#00779A\",\n        \"sideLabels\" => \"#00779A\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#3E6BFF\",\n    ],\n    \"shadow-orange\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#834400\",\n        \"stroke\" => \"#834400\",\n        \"ring\" => \"#834400\",\n        \"fire\" => \"#BB5502\",\n        \"currStreakNum\" => \"#EC861A\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#BB5502\",\n        \"sideLabels\" => \"#BB5502\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#EC861A\",\n    ],\n    \"shadow-purple\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#570182\",\n        \"stroke\" => \"#570182\",\n        \"ring\" => \"#570182\",\n        \"fire\" => \"#6F42C1\",\n        \"currStreakNum\" => \"#CA59FF\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#6F42C1\",\n        \"sideLabels\" => \"#6F42C1\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#CA59FF\",\n    ],\n    \"shadow-brown\" => [\n        \"background\" => \"#FFFFFF00\",\n        \"border\" => \"#31312D\",\n        \"stroke\" => \"#31312D\",\n        \"ring\" => \"#31312D\",\n        \"fire\" => \"#7D6642\",\n        \"currStreakNum\" => \"#BB9863\",\n        \"sideNums\" => \"#747474\",\n        \"currStreakLabel\" => \"#7D6642\",\n        \"sideLabels\" => \"#7D6642\",\n        \"dates\" => \"#747474\",\n        \"excludeDaysLabel\" => \"#BB9863\",\n    ],\n    \"github-dark-dimmed\" => [\n        \"background\" => \"#24292F\",\n        \"border\" => \"#373E47\",\n        \"stroke\" => \"#539BF5\",\n        \"ring\" => \"#539BF5\",\n        \"currStreakNum\" => \"#ADBAC7\",\n        \"fire\" => \"#539BF5\",\n        \"sideNums\" => \"#ADBAC7\",\n        \"currStreakLabel\" => \"#539BF5\",\n        \"sideLabels\" => \"#539BF5\",\n        \"dates\" => \"#ADBAC7\",\n        \"excludeDaysLabel\" => \"#78818A\",\n    ],\n    \"blue-navy\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#82AAFF\",\n        \"ring\" => \"#82AAFF\",\n        \"currStreakNum\" => \"#82AAFF\",\n        \"fire\" => \"#82AAFF\",\n        \"sideNums\" => \"#82AAFF\",\n        \"currStreakLabel\" => \"#82AAFF\",\n        \"sideLabels\" => \"#82AAFF\",\n        \"dates\" => \"#82AAFF\",\n        \"excludeDaysLabel\" => \"#82AAFF\",\n    ],\n    \"calm-pink\" => [\n        \"background\" => \"#2B2D40\",\n        \"border\" => \"#E1BC29\",\n        \"stroke\" => \"#E07A5F\",\n        \"ring\" => \"#E07A5F\",\n        \"currStreakNum\" => \"#EBCFB2\",\n        \"fire\" => \"#E07A5F\",\n        \"sideNums\" => \"#EBCFB2\",\n        \"currStreakLabel\" => \"#E07A5F\",\n        \"sideLabels\" => \"#E07A5F\",\n        \"dates\" => \"#E1BC29\",\n        \"excludeDaysLabel\" => \"#EBCFB2\",\n    ],\n    \"whatsapp-light\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#008069\",\n        \"ring\" => \"#008069\",\n        \"fire\" => \"#121B22\",\n        \"currStreakNum\" => \"#16D351\",\n        \"sideNums\" => \"#16D351\",\n        \"currStreakLabel\" => \"#121B22\",\n        \"sideLabels\" => \"#121B22\",\n        \"dates\" => \"#73828A\",\n        \"excludeDaysLabel\" => \"#73828A\",\n    ],\n    \"whatsapp-dark\" => [\n        \"background\" => \"#121B22\",\n        \"border\" => \"#1B2832\",\n        \"stroke\" => \"#273741\",\n        \"ring\" => \"#273741\",\n        \"fire\" => \"#E3E7EA\",\n        \"currStreakNum\" => \"#00A884\",\n        \"sideNums\" => \"#00A884\",\n        \"currStreakLabel\" => \"#E3E7EA\",\n        \"sideLabels\" => \"#E3E7EA\",\n        \"dates\" => \"#888D90\",\n        \"excludeDaysLabel\" => \"#888D90\",\n    ],\n    \"carbonfox\" => [\n        \"background\" => \"#161616\",\n        \"border\" => \"#282828\",\n        \"stroke\" => \"#EE5396\",\n        \"ring\" => \"#25BE6AC8\",\n        \"fire\" => \"#25BE6A\",\n        \"currStreakNum\" => \"#78A9FF\",\n        \"sideNums\" => \"#33B1FF\",\n        \"currStreakLabel\" => \"#DFDFE0\",\n        \"sideLabels\" => \"#DFDFE0\",\n        \"dates\" => \"#08BDBA\",\n        \"excludeDaysLabel\" => \"#EE5396\",\n    ],\n    \"dawnfox\" => [\n        \"background\" => \"#FAF4ED\",\n        \"border\" => \"#E5E9F0\",\n        \"stroke\" => \"#B4637A\",\n        \"ring\" => \"#618774C8\",\n        \"fire\" => \"#618774\",\n        \"currStreakNum\" => \"#286983\",\n        \"sideNums\" => \"#56949F\",\n        \"currStreakLabel\" => \"#575279\",\n        \"sideLabels\" => \"#575279\",\n        \"dates\" => \"#EA9D34\",\n        \"excludeDaysLabel\" => \"#B4637A\",\n    ],\n    \"dayfox\" => [\n        \"background\" => \"#F6F2EE\",\n        \"border\" => \"#F2E9E1\",\n        \"stroke\" => \"#A5222F\",\n        \"ring\" => \"#396847C8\",\n        \"fire\" => \"#396847\",\n        \"currStreakNum\" => \"#2848A9\",\n        \"sideNums\" => \"#287980\",\n        \"currStreakLabel\" => \"#352C24\",\n        \"sideLabels\" => \"#352C24\",\n        \"dates\" => \"#AC5402\",\n        \"excludeDaysLabel\" => \"#A5222F\",\n    ],\n    \"duskfox\" => [\n        \"background\" => \"#232136\",\n        \"border\" => \"#393552\",\n        \"stroke\" => \"#EB6F92\",\n        \"ring\" => \"#A3BE8CC8\",\n        \"fire\" => \"#A3BE8C\",\n        \"currStreakNum\" => \"#569FBA\",\n        \"sideNums\" => \"#9CCFD8\",\n        \"currStreakLabel\" => \"#E0DEF4\",\n        \"sideLabels\" => \"#E0DEF4\",\n        \"dates\" => \"#F6C177\",\n        \"excludeDaysLabel\" => \"#EB6F92\",\n    ],\n    \"nightfox\" => [\n        \"background\" => \"#192330\",\n        \"border\" => \"#393B44\",\n        \"stroke\" => \"#C94F6D\",\n        \"ring\" => \"#6C9581C8\",\n        \"fire\" => \"#81B29A\",\n        \"currStreakNum\" => \"#719CD6\",\n        \"sideNums\" => \"#63CDCF\",\n        \"currStreakLabel\" => \"#DFDFE0\",\n        \"sideLabels\" => \"#DFDFE0\",\n        \"dates\" => \"#DBC074\",\n        \"excludeDaysLabel\" => \"#C94F6D\",\n    ],\n    \"nordfox\" => [\n        \"background\" => \"#2E3440\",\n        \"border\" => \"#3B4252\",\n        \"stroke\" => \"#BF616A\",\n        \"ring\" => \"#A3BE8CC8\",\n        \"fire\" => \"#A3BE8C\",\n        \"currStreakNum\" => \"#81A1C1\",\n        \"sideNums\" => \"#88C0D0\",\n        \"currStreakLabel\" => \"#E5E9F0\",\n        \"sideLabels\" => \"#E5E9F0\",\n        \"dates\" => \"#EBCB8B\",\n        \"excludeDaysLabel\" => \"#BF616A\",\n    ],\n    \"terafox\" => [\n        \"background\" => \"#152528\",\n        \"border\" => \"#2F3239\",\n        \"stroke\" => \"#E85C51\",\n        \"ring\" => \"#7AA4A1C8\",\n        \"fire\" => \"#7AA4A1\",\n        \"currStreakNum\" => \"#5A93AA\",\n        \"sideNums\" => \"#A1CDD8\",\n        \"currStreakLabel\" => \"#EBEBEB\",\n        \"sideLabels\" => \"#EBEBEB\",\n        \"dates\" => \"#FDA47F\",\n        \"excludeDaysLabel\" => \"#E85C51\",\n    ],\n    \"iceberg\" => [\n        \"background\" => \"#1E2132\",\n        \"border\" => \"#33374C\",\n        \"stroke\" => \"#33374C\",\n        \"ring\" => \"#84A0C6\",\n        \"fire\" => \"#84A0C6\",\n        \"currStreakNum\" => \"#D2D4DE\",\n        \"sideNums\" => \"#327698\",\n        \"currStreakLabel\" => \"#D2D4DE\",\n        \"sideLabels\" => \"#D2D4DE\",\n        \"dates\" => \"#327698\",\n        \"excludeDaysLabel\" => \"#84A0C6\",\n    ],\n    \"whatsapp-light2\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#D8FDD2\",\n        \"stroke\" => \"#D8FDD2\",\n        \"ring\" => \"#767B7D\",\n        \"fire\" => \"#767B7D\",\n        \"currStreakNum\" => \"#1DAB61\",\n        \"sideNums\" => \"#1DAB61\",\n        \"currStreakLabel\" => \"#131A20\",\n        \"sideLabels\" => \"#131A20\",\n        \"dates\" => \"#767B7D\",\n        \"excludeDaysLabel\" => \"#E5A732\",\n    ],\n    \"whatsapp-dark2\" => [\n        \"background\" => \"#0B141B\",\n        \"border\" => \"#103629\",\n        \"stroke\" => \"#103629\",\n        \"ring\" => \"#858A8D\",\n        \"fire\" => \"#858A8D\",\n        \"currStreakNum\" => \"#21C063\",\n        \"sideNums\" => \"#21C063\",\n        \"currStreakLabel\" => \"#F7F8FA\",\n        \"sideLabels\" => \"#F7F8FA\",\n        \"dates\" => \"#858A8D\",\n        \"excludeDaysLabel\" => \"#FFD179\",\n    ],\n    \"travelers-theme\" => [\n        \"background\" => \"#150E1F\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#F28157\",\n        \"ring\" => \"#F28157\",\n        \"fire\" => \"#F28157\",\n        \"currStreakNum\" => \"#F2F2F2\",\n        \"sideNums\" => \"#F28157\",\n        \"currStreakLabel\" => \"#F2F2F2\",\n        \"sideLabels\" => \"#F2F2F2\",\n        \"dates\" => \"#F2F2F2\",\n        \"excludeDaysLabel\" => \"#464646\",\n    ],\n    \"youtube-dark\" => [\n        \"background\" => \"#0F0F0F\",\n        \"border\" => \"#272727\",\n        \"stroke\" => \"#272727\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FF0000\",\n        \"sideNums\" => \"#FF0000\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#BCBCBC\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"meta-light\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#1C2B33\",\n        \"stroke\" => \"#1C2B33\",\n        \"ring\" => \"#0081FB\",\n        \"fire\" => \"#006EE9\",\n        \"currStreakNum\" => \"#1C2B33\",\n        \"sideNums\" => \"#1C2B33\",\n        \"currStreakLabel\" => \"#1C2B33\",\n        \"sideLabels\" => \"#1C2B33\",\n        \"dates\" => \"#1C2B33\",\n        \"excludeDaysLabel\" => \"#1C2B33\",\n    ],\n    \"meta-dark\" => [\n        \"background\" => \"#1C2B33\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#0081FB\",\n        \"fire\" => \"#006EE9\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"dark-minimalist\" => [\n        \"background\" => \"#211F27\",\n        \"border\" => \"#B9B9C0\",\n        \"stroke\" => \"#B9B9C0\",\n        \"ring\" => \"#D484F4\",\n        \"fire\" => \"#D484F4\",\n        \"currStreakNum\" => \"#89B4FA\",\n        \"sideNums\" => \"#E5E5E5\",\n        \"currStreakLabel\" => \"#89B4FA\",\n        \"sideLabels\" => \"#E5E5E5\",\n        \"dates\" => \"#D0D1D3\",\n        \"excludeDaysLabel\" => \"#D0D1D3\",\n    ],\n    \"telegram\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#333333\",\n        \"stroke\" => \"#333333\",\n        \"ring\" => \"#0088CC\",\n        \"fire\" => \"#179CDE\",\n        \"currStreakNum\" => \"#179CDE\",\n        \"sideNums\" => \"#0088CC\",\n        \"currStreakLabel\" => \"#179CDE\",\n        \"sideLabels\" => \"#0088CC\",\n        \"dates\" => \"#0088CC\",\n        \"excludeDaysLabel\" => \"#0088CC\",\n    ],\n    \"taiga\" => [\n        \"background\" => \"#031B1B\",\n        \"border\" => \"#062E2F\",\n        \"stroke\" => \"#062E2F\",\n        \"ring\" => \"#1F8F92\",\n        \"fire\" => \"#1FBABE\",\n        \"currStreakNum\" => \"#1FBABE\",\n        \"sideNums\" => \"#1F8F92\",\n        \"currStreakLabel\" => \"#1F8F92\",\n        \"sideLabels\" => \"#1FBABE\",\n        \"dates\" => \"#1F8F92\",\n        \"excludeDaysLabel\" => \"#1F8F92\",\n    ],\n    \"telegram-gradient\" => [\n        \"background\" => \"45,0088CC,179CDE\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#FFFFFF\",\n        \"fire\" => \"#FFFFFF\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#FFFFFF\",\n        \"currStreakLabel\" => \"#FFFFFF\",\n        \"sideLabels\" => \"#FFFFFF\",\n        \"dates\" => \"#FFFFFF\",\n        \"excludeDaysLabel\" => \"#FFFFFF\",\n    ],\n    \"microsoft\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#737373\",\n        \"stroke\" => \"#737373\",\n        \"ring\" => \"#7FBA00\",\n        \"fire\" => \"#F25022\",\n        \"currStreakNum\" => \"#00A4EF\",\n        \"sideNums\" => \"#FFB900\",\n        \"currStreakLabel\" => \"#00A4EF\",\n        \"sideLabels\" => \"#FFB900\",\n        \"dates\" => \"#7FBA00\",\n        \"excludeDaysLabel\" => \"#7FBA00\",\n    ],\n    \"microsoft-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#737373\",\n        \"stroke\" => \"#737373\",\n        \"ring\" => \"#7FBA00\",\n        \"fire\" => \"#F25022\",\n        \"currStreakNum\" => \"#00A4EF\",\n        \"sideNums\" => \"#FFB900\",\n        \"currStreakLabel\" => \"#00A4EF\",\n        \"sideLabels\" => \"#FFB900\",\n        \"dates\" => \"#7FBA00\",\n        \"excludeDaysLabel\" => \"#7FBA00\",\n    ],\n    \"hacker-inverted\" => [\n        \"background\" => \"#20C20E\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#000000\",\n        \"ring\" => \"#000000\",\n        \"fire\" => \"#000000\",\n        \"currStreakNum\" => \"#000000\",\n        \"sideNums\" => \"#000000\",\n        \"currStreakLabel\" => \"#000000\",\n        \"sideLabels\" => \"#000000\",\n        \"dates\" => \"#000000\",\n        \"excludeDaysLabel\" => \"#000000\",\n    ],\n    \"rust-ferris-dark\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#FFFFFF\",\n        \"stroke\" => \"#F66200\",\n        \"ring\" => \"#F49600\",\n        \"fire\" => \"#F66200\",\n        \"currStreakNum\" => \"#F49600\",\n        \"sideNums\" => \"#CC3A00\",\n        \"currStreakLabel\" => \"#F49600\",\n        \"sideLabels\" => \"#CC3A00\",\n        \"dates\" => \"#F66200\",\n        \"excludeDaysLabel\" => \"#F66200\",\n    ],\n    \"rust-ferris-light\" => [\n        \"background\" => \"#FFFFFF\",\n        \"border\" => \"#000000\",\n        \"stroke\" => \"#F66200\",\n        \"ring\" => \"#F49600\",\n        \"fire\" => \"#F66200\",\n        \"currStreakNum\" => \"#F49600\",\n        \"sideNums\" => \"#CC3A00\",\n        \"currStreakLabel\" => \"#F49600\",\n        \"sideLabels\" => \"#CC3A00\",\n        \"dates\" => \"#F66200\",\n        \"excludeDaysLabel\" => \"#F66200\",\n    ],\n    \"cyber-streakglow\" => [\n        \"background\" => \"42,E20FEB,0D00EB\",\n        \"border\" => \"#00EBE1\",\n        \"stroke\" => \"#0FEB00\",\n        \"ring\" => \"#5AEB59\",\n        \"fire\" => \"#DDEB00\",\n        \"currStreakNum\" => \"#EBEBEB\",\n        \"sideNums\" => \"#D6EBC0\",\n        \"currStreakLabel\" => \"#46EB00\",\n        \"sideLabels\" => \"#64E8EB\",\n        \"dates\" => \"#EBEBEB\",\n        \"excludeDaysLabel\" => \"#A7EB3F\",\n    ],\n    \"vitesse\" => [\n        \"background\" => \"#000000\",\n        \"border\" => \"#4D9375\",\n        \"stroke\" => \"#5D99A9\",\n        \"ring\" => \"#4D9375\",\n        \"fire\" => \"#CB7676\",\n        \"currStreakNum\" => \"#B8A965\",\n        \"sideNums\" => \"#4D9375\",\n        \"currStreakLabel\" => \"#80A665\",\n        \"sideLabels\" => \"#80A665\",\n        \"dates\" => \"#BD976A\",\n        \"excludeDaysLabel\" => \"#758575DD\",\n    ],\n    \"everforest-dark\" => [\n        \"background\" => \"#2D353B\",\n        \"border\" => \"#4F585E\",\n        \"stroke\" => \"#4F585E\",\n        \"ring\" => \"#A7C080\",\n        \"fire\" => \"#A7C080\",\n        \"currStreakNum\" => \"#D3C6AA\",\n        \"sideNums\" => \"#A7C080\",\n        \"currStreakLabel\" => \"#D3C6AA\",\n        \"sideLabels\" => \"#A7C080\",\n        \"dates\" => \"#9DA9A0\",\n        \"excludeDaysLabel\" => \"#9DA9A0\",\n    ],\n    \"nord-aurora\" => [\n        \"background\" => \"#4C566A\",\n        \"border\" => \"#8FBCBB\",\n        \"stroke\" => \"#D8DEE9\",\n        \"ring\" => \"#A3BE8C\",\n        \"fire\" => \"#BF616A\",\n        \"currStreakNum\" => \"#A3BE8C\",\n        \"sideNums\" => \"#B48EAD\",\n        \"currStreakLabel\" => \"#EBCB8B\",\n        \"sideLabels\" => \"#D08770\",\n        \"dates\" => \"#88C0D0\",\n        \"excludeDaysLabel\" => \"#81A1C1\",\n    ],\n    \"dark-aura\" => [\n        \"background\" => \"#760A11\",\n        \"border\" => \"#310C69C5\",\n        \"stroke\" => \"#FF1D5E\",\n        \"ring\" => \"#C1184E\",\n        \"fire\" => \"#EB4511\",\n        \"currStreakNum\" => \"#EB5454\",\n        \"sideNums\" => \"#EB5454\",\n        \"currStreakLabel\" => \"#FF8F62\",\n        \"sideLabels\" => \"#FF8F62\",\n        \"dates\" => \"#EB5454\",\n        \"excludeDaysLabel\" => \"#758575DD\",\n    ],\n    \"everforest-light\" => [\n        \"background\" => \"#F2F4EF\",\n        \"border\" => \"#C7CCC2\",\n        \"stroke\" => \"#C7CCC2\",\n        \"ring\" => \"#7F9C6F\",\n        \"fire\" => \"#7F9C6F\",\n        \"currStreakNum\" => \"#55674E\",\n        \"sideNums\" => \"#7F9C6F\",\n        \"currStreakLabel\" => \"#55674E\",\n        \"sideLabels\" => \"#7F9C6F\",\n        \"dates\" => \"#8B9286\",\n        \"excludeDaysLabel\" => \"#8B9286\",\n    ],\n    \"oceanic-next\" => [\n        \"background\" => \"#1B2B34\",\n        \"border\" => \"#343D46\",\n        \"stroke\" => \"#4F5B66\",\n        \"ring\" => \"#6699CC\",\n        \"fire\" => \"#EC5F67\",\n        \"currStreakNum\" => \"#99C794\",\n        \"sideNums\" => \"#6699CC\",\n        \"currStreakLabel\" => \"#FAC863\",\n        \"sideLabels\" => \"#5FB3B3\",\n        \"dates\" => \"#A7ADBA\",\n        \"excludeDaysLabel\" => \"#A7ADBA\",\n    ],\n    \"sakura-x\" => [\n        \"background\" => \"#1A1B26\",\n        \"border\" => \"#9D4EDD\",\n        \"stroke\" => \"#FFFFFF\",\n        \"ring\" => \"#9D4EDD\",\n        \"fire\" => \"#E5C07B\",\n        \"currStreakNum\" => \"#FFFFFF\",\n        \"sideNums\" => \"#9D4EDD\",\n        \"currStreakLabel\" => \"#E5C07B\",\n        \"sideLabels\" => \"#E5C07B\",\n        \"dates\" => \"#C0CAF5\",\n        \"excludeDaysLabel\" => \"#C0CAF5\",\n    ],\n    \"kanagawa-paper\" => [\n        \"background\" => \"#1F1F28\",\n        \"border\" => \"#54546D\",\n        \"stroke\" => \"#727169\",\n        \"ring\" => \"#7FB4CA\",\n        \"fire\" => \"#E46876\",\n        \"currStreakNum\" => \"#98BB6C\",\n        \"sideNums\" => \"#7E9CD8\",\n        \"currStreakLabel\" => \"#E6C384\",\n        \"sideLabels\" => \"#7AA89F\",\n        \"dates\" => \"#C4B28A\",\n        \"excludeDaysLabel\" => \"#C8C093\",\n    ],\n];\n"
  },
  {
    "path": "src/translations.php",
    "content": "<?php\n\n/**\n * Locales\n * -------\n * For a list of supported locale codes, see https://gist.github.com/DenverCoder1/f61147ba26bfcf7c3bf605af7d3382d5\n *\n * Date Format\n * -----------\n * If the default date format for the locale displays correctly, you should omit the date_format parameter.\n * Supplying a date format is optional and will be used instead of the default locale date format.\n *\n * Different year   Same year   Format string\n * --------------   ---------   -------------\n * 10/8/2016        10/8        j/n[/Y]\n * 8/10/2016        8/10        n/j[/Y]\n * 2016.8.10        8.10        [Y.]n.j\n *\n * For info on valid date_format strings, see https://github.com/DenverCoder1/github-readme-streak-stats#date-formats\n *\n * Right-to-Left Language Support\n * ------------------------------\n * To enable right-to-left language support, add `\"rtl\" => true` to the locale array (see \"he\" for an example).\n *\n * Comma Separator\n * ---------------\n * To change the comma separator in the enumeration of excluded days, add `\"comma_separator\" => \", \"` to the locale array with the desired separator as the value.\n *\n * Aliases\n * -------\n * To add an alias for a locale, add the alias as a key to the locale array with the locale it should redirect to as the value.\n * For example, if \"zh\" is an alias for \"zh_Hans\", then `\"zh\" => \"zh_Hans\"` would be added to the locale array.\n */\n\nreturn [\n    // \"en\" is the default locale\n    \"en\" => [\n        \"Total Contributions\" => \"Total Contributions\",\n        \"Current Streak\" => \"Current Streak\",\n        \"Longest Streak\" => \"Longest Streak\",\n        \"Week Streak\" => \"Week Streak\",\n        \"Longest Week Streak\" => \"Longest Week Streak\",\n        \"Present\" => \"Present\",\n        \"Excluding {days}\" => \"Excluding {days}\",\n    ],\n    // Locales below are sorted alphabetically\n    \"am\" => [\n        \"Total Contributions\" => \"ጠቅላላ አስተዋጽዖዎች\",\n        \"Current Streak\" => \"የአሁን ድግግሞሽ\",\n        \"Longest Streak\" => \"በጣም ረጅሙ ድግግሞሽ\",\n        \"Week Streak\" => \"የሳምንት ድግግሞሽ\",\n        \"Longest Week Streak\" => \"በጣም ረጅሙ የሳምንት ድግግሞሽ\",\n        \"Present\" => \"ያሁኑ\",\n        \"Excluding {days}\" => \"ሳይጨምር {days}\",\n    ],\n    \"ar\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"إجمالي المساهمات\",\n        \"Current Streak\" => \"السلسلة المتتالية الحالية\",\n        \"Longest Streak\" => \"أُطول سلسلة متتالية\",\n        \"Week Streak\" => \"السلسلة المتتالية الأُسبوعية\",\n        \"Longest Week Streak\" => \"أُطول سلسلة متتالية أُسبوعية\",\n        \"Present\" => \"الحاضر\",\n        \"Excluding {days}\" => \"باستثناء {days}\",\n        \"comma_separator\" => \"، \",\n    ],\n    \"as\" => [\n        \"Total Contributions\" => \"মুঠ বৰঙণি\",\n        \"Current Streak\" => \"বৰ্তমান ষ্ট্ৰীক\",\n        \"Longest Streak\" => \"দীৰ্ঘতম ষ্ট্ৰীক\",\n        \"Week Streak\" => \"সপ্তাহ ষ্ট্ৰীক\",\n        \"Longest Week Streak\" => \"দীৰ্ঘতম সপ্তাহ ষ্ট্ৰীক\",\n        \"Present\" => \"বৰ্তমান\",\n        \"Excluding {days}\" => \"{days} বাদ দি\",\n    ],\n    \"bg\" => [\n        \"Total Contributions\" => \"Общ принос\",\n        \"Current Streak\" => \"Дневна серия\",\n        \"Longest Streak\" => \"Най-дълга дневна серия\",\n        \"Week Streak\" => \"Седмична серия\",\n        \"Longest Week Streak\" => \"Най-дълга седмична серия\",\n        \"Present\" => \"Сега\",\n    ],\n    \"bho\" => [\n        \"Total Contributions\" => \"कुल योगदान\",\n        \"Current Streak\" => \"चालू रोजाना योगदान\",\n        \"Longest Streak\" => \"सबसे लंबा रोजाना योगदान\",\n        \"Week Streak\" => \"सप्ताहिक योगदान\",\n        \"Longest Week Streak\" => \"सबसे लंबा सप्ताहिक योगदान\",\n        \"Present\" => \"आज ले\",\n        \"Excluding {days}\" => \"{days} के छोड़के\",\n    ],\n    \"bn\" => [\n        \"Total Contributions\" => \"মোট অবদান\",\n        \"Current Streak\" => \"বর্তমান স্ট্রিক\",\n        \"Longest Streak\" => \"দীর্ঘতম স্ট্রিক\",\n        \"Week Streak\" => \"সপ্তাহ স্ট্রিক\",\n        \"Longest Week Streak\" => \"দীর্ঘতম সপ্তাহ স্ট্রিক\",\n        \"Present\" => \"বর্তমান\",\n        \"Excluding {days}\" => \"{days} বাদে\",\n    ],\n    \"ca\" => [\n        \"Total Contributions\" => \"Aportacions totals\",\n        \"Current Streak\" => \"Ratxa actual\",\n        \"Longest Streak\" => \"Ratxa més llarga\",\n        \"Week Streak\" => \"Ratxa setmanal\",\n        \"Longest Week Streak\" => \"Ratxa setmanal més llarga\",\n        \"Present\" => \"Actual\",\n        \"Excluding {days}\" => \"Excloent {days}\",\n    ],\n    \"ceb\" => [\n        \"Total Contributions\" => \"Kinatibuk-ang Kontribusyon\",\n        \"Current Streak\" => \"Kasamtangan nga Streak\",\n        \"Longest Streak\" => \"Pinakataas nga Streak\",\n        \"Week Streak\" => \"Sinemana nga Streak\",\n        \"Longest Week Streak\" => \"Pinakataas nga Semana nga Streak\",\n        \"Present\" => \"Karon\",\n        \"Excluding {days}\" => \"Wala'y Labot {days}\",\n    ],\n    \"da\" => [\n        \"Total Contributions\" => \"Samlet antal bidrag\",\n        \"Current Streak\" => \"Bidrag i træk\",\n        \"Longest Streak\" => \"Flest bidrag i træk\",\n        \"Week Streak\" => \"Ugentlige bidrag i træk\",\n        \"Longest Week Streak\" => \"Flest ugentlige bidrag i træk\",\n        \"Present\" => \"Nuværende\",\n        \"Excluding {days}\" => \"Ekskluderer {days}\",\n    ],\n    \"de\" => [\n        \"Total Contributions\" => \"Gesamte Beiträge\",\n        \"Current Streak\" => \"Aktuelle Serie\",\n        \"Longest Streak\" => \"Längste Serie\",\n        \"Week Streak\" => \"Wochenserie\",\n        \"Longest Week Streak\" => \"Längste Wochenserie\",\n        \"Present\" => \"Heute\",\n        \"Excluding {days}\" => \"Ausgenommen {days}\",\n    ],\n    \"el\" => [\n        \"Total Contributions\" => \"Συνολικές Συνεισφορές\",\n        \"Current Streak\" => \"Τρέχουσα Σειρά\",\n        \"Longest Streak\" => \"Μεγαλύτερη Σειρά\",\n        \"Week Streak\" => \"Εβδομαδιαία Σειρά\",\n        \"Longest Week Streak\" => \"Μεγαλύτερη Εβδομαδιαία Σειρά\",\n        \"Present\" => \"Σήμερα\",\n        \"Excluding {days}\" => \"Εξαιρούνται {days}\",\n    ],\n    \"es\" => [\n        \"Total Contributions\" => \"Contribuciones Totales\",\n        \"Current Streak\" => \"Racha Actual\",\n        \"Longest Streak\" => \"Racha Más Larga\",\n        \"Week Streak\" => \"Racha Semanal\",\n        \"Longest Week Streak\" => \"Racha Semanal Más Larga\",\n        \"Present\" => \"Presente\",\n        \"Excluding {days}\" => \"Excluyendo {days}\",\n    ],\n    \"fa\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"مجموع مشارکت ها\",\n        \"Current Streak\" => \"پی‌رفت فعلی\",\n        \"Longest Streak\" => \"طولانی ترین پی‌رفت\",\n        \"Week Streak\" => \"پی‌رفت هفته\",\n        \"Longest Week Streak\" => \"طولانی ترین پی‌رفت هفته\",\n        \"Present\" => \"اکنون\",\n        \"Excluding {days}\" => \"{days} مستثنی کردن\",\n        \"comma_separator\" => \"، \",\n    ],\n    \"fil\" => [\n        \"Total Contributions\" => \"Kabuuang Kontribusyon\",\n        \"Current Streak\" => \"Kasalukuyang Streak\",\n        \"Longest Streak\" => \"Pinakamahabang Streak\",\n        \"Week Streak\" => \"Linggong Streak\",\n        \"Longest Week Streak\" => \"Pinakamahabang Linggong Streak\",\n        \"Present\" => \"Kasalukuyan\",\n        \"Excluding {days}\" => \"Hindi Kasama {days}\",\n    ],\n    \"fr\" => [\n        \"Total Contributions\" => \"Contributions totales\",\n        \"Current Streak\" => \"Séquence actuelle\",\n        \"Longest Streak\" => \"Plus longue séquence\",\n        \"Week Streak\" => \"Séquence de la semaine\",\n        \"Longest Week Streak\" => \"Plus longue séquence hebdomadaire\",\n        \"Present\" => \"Aujourd'hui\",\n        \"Excluding {days}\" => \"À l'exclusion de {days}\",\n    ],\n    \"gu\" => [\n        \"Total Contributions\" => \"કુલ યોગદાન\",\n        \"Current Streak\" => \"સતત દૈનિક યોગદાન\",\n        \"Longest Streak\" => \"સૌથી લાંબુ દૈનિક યોગદાન\",\n        \"Week Streak\" => \"અઠવાડીક યોગદાન\",\n        \"Longest Week Streak\" => \"સૌથી લાંબુ અઠવાડીક યોગદાન\",\n        \"Present\" => \"અત્યાર સુધી\",\n        \"Excluding {days}\" => \"સિવાય {days}\",\n    ],\n    \"he\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"סכום התרומות\",\n        \"Current Streak\" => \"רצף נוכחי\",\n        \"Longest Streak\" => \"רצף הכי ארוך\",\n        \"Week Streak\" => \"רצף שבועי\",\n        \"Longest Week Streak\" => \"רצף שבועי הכי ארוך\",\n        \"Present\" => \"היום\",\n        \"Excluding {days}\" => \"לא כולל {days}\",\n    ],\n    \"hi\" => [\n        \"Total Contributions\" => \"कुल योगदान\",\n        \"Current Streak\" => \"निरंतर दैनिक योगदान\",\n        \"Longest Streak\" => \"सबसे लंबा दैनिक योगदान\",\n        \"Week Streak\" => \"सप्ताहिक योगदान\",\n        \"Longest Week Streak\" => \"दीर्घ साप्ताहिक योगदान\",\n        \"Present\" => \"आज तक\",\n        \"Excluding {days}\" => \"के सिवा {days}\",\n    ],\n    \"ht\" => [\n        \"Total Contributions\" => \"kontribisyon total\",\n        \"Current Streak\" => \"tras aktyèl\",\n        \"Longest Streak\" => \"tras ki pi long\",\n        \"Week Streak\" => \"tras semèn\",\n        \"Longest Week Streak\" => \"pi long tras semèn\",\n        \"Present\" => \"Prezan\",\n    ],\n    \"hu\" => [\n        \"Total Contributions\" => \"Összes hozzájárulás\",\n        \"Current Streak\" => \"Jelenlegi sorozat\",\n        \"Longest Streak\" => \"Leghosszabb sorozat\",\n        \"Week Streak\" => \"Heti sorozat\",\n        \"Longest Week Streak\" => \"Leghosszabb heti sorozat\",\n        \"Present\" => \"Jelen\",\n        \"Excluding {days}\" => \"Kivéve {days}\",\n    ],\n    \"hy\" => [\n        \"Total Contributions\" => \"Ընդհանուր\\nներդրումը\",\n        \"Current Streak\" => \"Ընթացիկ շարք\",\n        \"Longest Streak\" => \"Ամենաերկար շարք\",\n        \"Week Streak\" => \"Ընթացիկ\\nշաբաթների շարք\",\n        \"Longest Week Streak\" => \"Ամենաերկար\\nշաբաթների շարք\",\n        \"Present\" => \"Այժմ\",\n    ],\n    \"id\" => [\n        \"Total Contributions\" => \"Total Kontribusi\",\n        \"Current Streak\" => \"Aksi Saat Ini\",\n        \"Longest Streak\" => \"Aksi Terpanjang\",\n        \"Week Streak\" => \"Aksi Mingguan\",\n        \"Longest Week Streak\" => \"Aksi Mingguan Terpanjang\",\n        \"Present\" => \"Sekarang\",\n        \"Excluding {days}\" => \"Kecuali {days}\",\n    ],\n    \"it\" => [\n        \"Total Contributions\" => \"Contributi Totali\",\n        \"Current Streak\" => \"Serie Corrente\",\n        \"Longest Streak\" => \"Serie più Lunga\",\n        \"Week Streak\" => \"Serie Settimanale\",\n        \"Longest Week Streak\" => \"Serie Settimanale più Lunga\",\n        \"Present\" => \"Presente\",\n        \"Excluding {days}\" => \"Escludendo {days}\",\n    ],\n    \"ja\" => [\n        \"date_format\" => \"[Y.]n.j\",\n        \"Total Contributions\" => \"総ｺﾝﾄﾘﾋﾞｭｰｼｮﾝ数\",\n        \"Current Streak\" => \"現在のストリーク\",\n        \"Longest Streak\" => \"最長のストリーク\",\n        \"Week Streak\" => \"週間ストリーク\",\n        \"Longest Week Streak\" => \"最長の週間ストリーク\",\n        \"Present\" => \"今\",\n        \"Excluding {days}\" => \"{days}を除く\",\n        \"comma_separator\" => \"・\",\n    ],\n    \"jv\" => [\n        \"Total Contributions\" => \"Total Kontribusi\",\n        \"Current Streak\" => \"Tumindak Saiki\",\n        \"Longest Streak\" => \"Tumindak Paling Dawa\",\n        \"Week Streak\" => \"Tumindak Saben Minggu\",\n        \"Longest Week Streak\" => \"Tumindak Saben Minggu Paling Dawa\",\n        \"Present\" => \"Saiki\",\n        \"Excluding {days}\" => \"Ora kelebu {days}\",\n    ],\n    \"kn\" => [\n        \"Total Contributions\" => \"ಒಟ್ಟು ಕೊಡುಗೆ\",\n        \"Current Streak\" => \"ಪ್ರಸ್ತುತ ಸ್ಟ್ರೀಕ್\",\n        \"Longest Streak\" => \"ಅತ್ಯಧಿಕ ಸ್ಟ್ರೀಕ್\",\n        \"Week Streak\" => \"ವಾರದ ಸ್ಟ್ರೀಕ್\",\n        \"Longest Week Streak\" => \"ಅತ್ಯಧಿಕ ವಾರದ ಸ್ಟ್ರೀಕ್\",\n        \"Present\" => \"ಪ್ರಸ್ತುತ\",\n        \"Excluding {days}\" => \"ಹೊರತುಪಡಿಸಿ {days}\",\n    ],\n    \"ko\" => [\n        \"Total Contributions\" => \"총 기여 수\",\n        \"Current Streak\" => \"현재 연속 기여 수\",\n        \"Longest Streak\" => \"최장 연속 기여 수\",\n        \"Week Streak\" => \"주간 연속 기여 수\",\n        \"Longest Week Streak\" => \"최장 주간 연속 기여 수\",\n        \"Present\" => \"현재\",\n        \"Excluding {days}\" => \"{days}를 제외하고\",\n    ],\n    \"mai\" => [\n        \"Total Contributions\" => \"कुल योगदान\",\n        \"Current Streak\" => \"वर्तमान योगदान क्रम\",\n        \"Longest Streak\" => \"सभसँ लम्बा योगदान क्रम\",\n        \"Week Streak\" => \"साप्ताहिक योगदान क्रम\",\n        \"Longest Week Streak\" => \"सभसँ लम्बा साप्ताहिक योगदान क्रम\",\n        \"Present\" => \"एखन धरि\",\n        \"Excluding {days}\" => \"{days} कए छोड़िकय\",\n    ],\n    \"mal\" => [\n        \"Total Contributions\" => \"മൊത്തം സംഭാവനകൾ\",\n        \"Current Streak\" => \"നിലവിലെ സ്ട്രീക്ക്\",\n        \"Longest Streak\" => \"ഏറ്റവും ദൈർഘ്യമേറിയ സ്ട്രീക്ക്\",\n        \"Week Streak\" => \"പ്രതിവാര സ്ട്രീക്ക്\",\n        \"Longest Week Streak\" => \"ദൈർഘ്യമേറിയ ആഴ്ച സ്‌ട്രീക്ക്\",\n        \"Present\" => \"ഇപ്പം\",\n        \"Excluding {days}\" => \"{days} ഒഴികെ\",\n        \"comma_separator\" => \"、\",\n    ],\n    \"mi\" => [\n        \"Total Contributions\" => \"Tapeke Tākoha\",\n        \"Current Streak\" => \"Raupapa Nāianei\",\n        \"Longest Streak\" => \"Raupapa Roa Rawa\",\n        \"Week Streak\" => \"Raupapa Wiki\",\n        \"Longest Week Streak\" => \"Raupapa Wiki Roa Rawa\",\n        \"Present\" => \"O Nāianei\",\n        \"Excluding {days}\" => \"Haunga {ngā rā}\",\n    ],\n    \"mr\" => [\n        \"Total Contributions\" => \"एकूण योगदान\",\n        \"Current Streak\" => \"साध्यकालीन सातत्यता\",\n        \"Longest Streak\" => \"दीर्घकालीन सातत्यता\",\n        \"Week Streak\" => \"साप्ताहिक सातत्यता\",\n        \"Longest Week Streak\" => \"दीर्घकालीन साप्ताहिक सातत्यता\",\n        \"Present\" => \"आज पर्यंत\",\n        \"Excluding {days}\" => \"वगळून {days}\",\n    ],\n    \"ms\" => [\n        \"Total Contributions\" => \"Jumlah Sumbangan\",\n        \"Current Streak\" => \"Tindakan Semasa\",\n        \"Longest Streak\" => \"Tindakan Terpanjang\",\n        \"Week Streak\" => \"Tindakan Setiap Minggu\",\n        \"Longest Week Streak\" => \"Tindakan Setiap Minggu Terpanjang\",\n        \"Present\" => \"Sekarang\",\n        \"Excluding {days}\" => \"Kecuali {days}\",\n    ],\n    \"ms_ID\" => [\n        \"Total Contributions\" => \"Total Kontribusi\",\n        \"Current Streak\" => \"Rangkaian Saat Ini\",\n        \"Longest Streak\" => \"Rangkaian Terpanjang\",\n        \"Week Streak\" => \"Rangkaian Mingguan\",\n        \"Longest Week Streak\" => \"Rangkaian Mingguan Terpanjang\",\n        \"Present\" => \"Sekarang\",\n        \"Excluding {days}\" => \"Tidak termasuk {days}\",\n    ],\n    \"my\" => [\n        \"Total Contributions\" => \"စုစုပေါင်း ပံ့ပိုးမှုများ\",\n        \"Current Streak\" => \"ယနေ့ထိ မပျက်မကွက် ပံ့ပိုးမှုရက်ပေါင်း\",\n        \"Longest Streak\" => \"အကြာဆုံးမပျက်မကွက် ပံ့ပိုးမှုရက်ပေါင်း\",\n        \"Week Streak\" => \"အပတ်စဉ် ပံ့ပိုးမှု\",\n        \"Longest Week Streak\" => \"အကြာဆုံးမပျက်မကွက် ပံ့ပိုးမှုအပတ်ပေါင်း\",\n        \"Present\" => \"လက်ရှိ\",\n        \"Excluding {days}\" => \"{days} မှလွဲ၍\",\n    ],\n    \"ne\" => [\n        \"Total Contributions\" => \"कुल योगदान\",\n        \"Current Streak\" => \"हालको दैनिक योगदान\",\n        \"Longest Streak\" => \"सबैभन्दा लामो दैनिक योगदान\",\n        \"Week Streak\" => \"सप्ताहिक योगदान\",\n        \"Longest Week Streak\" => \"सबैभन्दा लामो साप्ताहिक योगदान\",\n        \"Present\" => \"आज सम्म\",\n        \"Excluding {days}\" => \"बाहेक {days}\",\n    ],\n    \"nl\" => [\n        \"Total Contributions\" => \"Totale Bijdrage\",\n        \"Current Streak\" => \"Huidige Serie\",\n        \"Longest Streak\" => \"Langste Serie\",\n        \"Week Streak\" => \"Week Serie\",\n        \"Longest Week Streak\" => \"Langste Week Serie\",\n        \"Present\" => \"Vandaag\",\n        \"Excluding {days}\" => \"Exclusief {days}\",\n    ],\n    \"no\" => [\n        \"Total Contributions\" => \"Totalt Antall Bidrag\",\n        \"Current Streak\" => \"Nåværende\\nBidragsrekke\",\n        \"Longest Streak\" => \"Lengste Bidragsrekke\",\n        \"Week Streak\" => \"Ukentlig\\nBidragsrekke\",\n        \"Longest Week Streak\" => \"Lengste Ukentlige\\nBidragsrekke\",\n        \"Present\" => \"I dag\",\n        \"Excluding {days}\" => \"Ekskluderer {days}\",\n    ],\n    \"pa\" => [\n        \"Total Contributions\" => \"ਕੁੱਲ ਯੋਗਦਾਨ\",\n        \"Current Streak\" => \"ਮੌਜੂਦਾ ਲਗਾਤਾਰ ਦਿਨ\",\n        \"Longest Streak\" => \"ਸਭ ਤੋਂ ਲੰਬੀ ਲਗਾਤਾਰ ਸਿਰੀਂ\",\n        \"Week Streak\" => \"ਹਫ਼ਤਾ ਲਗਾਤਾਰ ਸਿਰੀਂ\",\n        \"Longest Week Streak\" => \"ਸਭ ਤੋਂ ਲੰਬੀ ਹਫ਼ਤਾਵਾਰੀ ਸਿਰੀਂ\",\n        \"Present\" => \"ਮੌਜੂਦ\",\n        \"Excluding {days}\" => \"{days} ਨੂੰ ਛੱਡ ਕੇ\",\n    ],\n    \"pl\" => [\n        \"Total Contributions\" => \"Suma Kontrybucji\",\n        \"Current Streak\" => \"Aktualna Seria\",\n        \"Longest Streak\" => \"Najdłuższa Seria\",\n        \"Week Streak\" => \"Seria Tygodni\",\n        \"Longest Week Streak\" => \"Najdłuższa Seria Tygodni\",\n        \"Present\" => \"Dziś\",\n        \"Excluding {days}\" => \"Wykluczono {days}\",\n    ],\n    \"ps\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"ټولې ونډې\",\n        \"Current Streak\" => \"اوسنی پرمختګ\",\n        \"Longest Streak\" => \"تر ټولو اوږد پرمختګ\",\n        \"Week Streak\" => \"د اونۍ پرمختګ\",\n        \"Longest Week Streak\" => \"د اونۍ تر ټولو اوږد پرمختګ\",\n        \"Present\" => \"اوس\",\n        \"comma_separator\" => \"، \",\n        \"Excluding {days}\" => \"پرته {days}\",\n    ],\n    \"pt\" => [\n        \"Total Contributions\" => \"Contribuições Totais\",\n        \"Current Streak\" => \"Sequência Atual\",\n        \"Longest Streak\" => \"Maior Sequência\",\n        \"Week Streak\" => \"Sequência da Semana\",\n        \"Longest Week Streak\" => \"Maior Sequência da Semana\",\n        \"Present\" => \"Presente\",\n        \"Excluding {days}\" => \"Excluindo {days}\",\n    ],\n    \"pt_BR\" => [\n        \"Total Contributions\" => \"Total de Contribuições\",\n        \"Current Streak\" => \"Sequência Atual\",\n        \"Longest Streak\" => \"Maior Sequência\",\n        \"Week Streak\" => \"Sequência Semanal\",\n        \"Longest Week Streak\" => \"Maior Sequência Semanal\",\n        \"Present\" => \"Presente\",\n        \"Excluding {days}\" => \"Exceto {days}\",\n    ],\n    \"ro\" => [\n        \"Total Contributions\" => \"Contribuții totale\",\n        \"Current Streak\" => \"Perioada curentă\",\n        \"Longest Streak\" => \"Cea mai lungă perioadă\",\n        \"Week Streak\" => \"Perioada săptămânală\",\n        \"Longest Week Streak\" => \"Cea mai lungă perioadă săptămânală\",\n        \"Present\" => \"Prezent\",\n        \"Excluding {days}\" => \"Excludând {days}\",\n    ],\n    \"ru\" => [\n        \"Total Contributions\" => \"Общий вклад\",\n        \"Current Streak\" => \"Текущая серия\",\n        \"Longest Streak\" => \"Самая длинная серия\",\n        \"Week Streak\" => \"Текущая серия недель\",\n        \"Longest Week Streak\" => \"Самая длинная серия недель\",\n        \"Present\" => \"Сейчас\",\n        \"Excluding {days}\" => \"Не включая {days}\",\n    ],\n    \"rw\" => [\n        \"Total Contributions\" => \"Imisanzu yose\",\n        \"Current Streak\" => \"Igihe gishize ntaguhagarara\",\n        \"Longest Streak\" => \"Igihe cyirecyire cyashize ntaguhagarara\",\n        \"Week Streak\" => \"Igihe gishize ntaguhagarara mu cyumweru\",\n        \"Longest Week Streak\" => \"Igihe cyirecyire cyashize ntaguhagarara mu byumweru\",\n        \"Present\" => \"None\",\n    ],\n    \"sa\" => [\n        \"Total Contributions\" => \"कुल योगदानम्\",\n        \"Current Streak\" => \"क्रमशः दिवसान् चालयन्तु\",\n        \"Longest Streak\" => \"दीर्घतमाः क्रमशः दिवसाः\",\n        \"Week Streak\" => \"निरन्तरसप्ताहाः\",\n        \"Longest Week Streak\" => \"दीर्घतमाः निरन्तरसप्ताहाः\",\n        \"Present\" => \"वर्तमान\",\n        \"Excluding {days}\" => \"बहिष्करणम् {days}\",\n    ],\n    \"sd_PK\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"کل حصہ داری\",\n        \"Current Streak\" => \"موجوده سلسلو\",\n        \"Longest Streak\" => \"تمام پري جو سلسلو\",\n        \"Week Streak\" => \"ھفتي جو سلسلو\",\n        \"Longest Week Streak\" => \"تمام پري جو ھفتيوار سلسلو\",\n        \"Present\" => \"موجوده\",\n        \"Excluding {days}\" => \"نڪتل {days}\",\n        \"comma_separator\" => \"، \",\n    ],\n    \"sr\" => \"sr_Cyrl\",\n    \"sr_Cyrl\" => [\n        \"Total Contributions\" => \"Укупно доприноса\",\n        \"Current Streak\" => \"Тренутна серија\",\n        \"Longest Streak\" => \"Најдужа серија\",\n        \"Week Streak\" => \"Недељна серија\",\n        \"Longest Week Streak\" => \"Најдужа недељна серија\",\n        \"Present\" => \"Данас\",\n        \"Excluding {days}\" => \"Искључујући {days}\",\n    ],\n    \"sr_Latn\" => [\n        \"Total Contributions\" => \"Ukupno doprinosa\",\n        \"Current Streak\" => \"Trenutna serija\",\n        \"Longest Streak\" => \"Najduža serija\",\n        \"Week Streak\" => \"Nedeljna serija\",\n        \"Longest Week Streak\" => \"Najduža nedeljna serija\",\n        \"Present\" => \"Danas\",\n        \"Excluding {days}\" => \"Isključujući {days}\",\n    ],\n    \"su\" => [\n        \"Total Contributions\" => \"Total Kontribusi\",\n        \"Current Streak\" => \"Aksi Ayeuna\",\n        \"Longest Streak\" => \"Aksi Pangpanjangna\",\n        \"Week Streak\" => \"Aksi Unggal Minggon\",\n        \"Longest Week Streak\" => \"Aksi Unggal Minggon Pangpanjangna\",\n        \"Present\" => \"Ayeuna\",\n        \"Excluding {days}\" => \"Teu Kaasup {days}\",\n    ],\n    \"sv\" => [\n        \"Total Contributions\" => \"Totalt antal uppladningar\",\n        \"Current Streak\" => \"Dagar uppladdat i rad just nu\",\n        \"Longest Streak\" => \"Längst antal dagar uppladdat i rad\",\n        \"Week Streak\" => \"Antal veckor i rad\",\n        \"Longest Week Streak\" => \"Längst antal veckor i rad\",\n        \"Present\" => \"Just nu\",\n        \"Excluding {days}\" => \"Utom {dagar}\",\n    ],\n    \"sw\" => [\n        \"Total Contributions\" => \"Jumla ya Michango\",\n        \"Current Streak\" => \"Mfululizo wa sasa\",\n        \"Longest Streak\" => \"Mfululizo mrefu zaidi\",\n        \"Week Streak\" => \"Mfululizo wa wiki\",\n        \"Longest Week Streak\" => \"Mfululizo mrefu zaidi wa wiki\",\n        \"Present\" => \"Sasa\",\n        \"Excluding {days}\" => \"Ukiondoa {days}\",\n    ],\n    \"ta\" => [\n        \"Total Contributions\" => \"மொத்த\\nபங்களிப்புகள்\",\n        \"Current Streak\" => \"மிக சமீபத்திய பங்களிப்புகள்\",\n        \"Longest Streak\" => \"நீண்ட\\nபங்களிப்புகள்\",\n        \"Week Streak\" => \"வார\\nபங்களிப்புகள்\",\n        \"Longest Week Streak\" => \"நீண்ட வார\\nபங்களிப்புகள்\",\n        \"Present\" => \"இன்றுவரை\",\n        \"Excluding {days}\" => \"{days} தவிர\",\n    ],\n    \"tcy\" => [\n        \"Total Contributions\" => \"ಒಟ್ಟು ಕೊಡುಗೆ\",\n        \"Current Streak\" => \"ಪ್ರಸ್ತುತ ಸ್ಟ್ರೀಕ್\",\n        \"Longest Streak\" => \"ಅತ್ಯಧಿಕ ಸ್ಟ್ರೀಕ್\",\n        \"Week Streak\" => \"ವಾರದ ಸ್ಟ್ರೀಕ್\",\n        \"Longest Week Streak\" => \"ಅತ್ಯಧಿಕ ವಾರದ ಸ್ಟ್ರೀಕ್\",\n        \"Present\" => \"ಇತ್ತೆಗ್\",\n        \"Excluding {days}\" => \"{days} ಬುಡ್ದು\",\n    ],\n    \"te\" => [\n        \"Total Contributions\" => \"మొత్తం సహకారం\",\n        \"Current Streak\" => \"ప్రస్తుత సహకారం\",\n        \"Longest Streak\" => \"అత్యధిక సహకారం\",\n        \"Week Streak\" => \"వారపు సహకారం\",\n        \"Longest Week Streak\" => \"అత్యధిక వారపు సహకారం\",\n        \"Present\" => \"ప్రస్తుతం\",\n        \"Excluding {days}\" => \"{days} మినహా\",\n    ],\n    \"th\" => [\n        \"Total Contributions\" => \"คอนทริบิ้วต์ทั้งหมด\",\n        \"Current Streak\" => \"สตรีคปัจจุบัน\",\n        \"Longest Streak\" => \"สตรีคที่ยาวนานที่สุด\",\n        \"Week Streak\" => \"สตรีคประจำสัปดาห์\",\n        \"Longest Week Streak\" => \"สตรีคประจำสัปดาห์\\nที่ยาวนานที่สุด\",\n        \"Present\" => \"ปัจจุบัน\",\n        \"Excluding {days}\" => \"ยกเว้น {days}\",\n    ],\n    \"tr\" => [\n        \"Total Contributions\" => \"Toplam Katkı\",\n        \"Current Streak\" => \"Güncel Seri\",\n        \"Longest Streak\" => \"En Uzun Seri\",\n        \"Week Streak\" => \"Haftalık Seri\",\n        \"Longest Week Streak\" => \"En Uzun Haftalık Seri\",\n        \"Present\" => \"Şu an\",\n        \"Excluding {days}\" => \"Hariç {days}\",\n    ],\n    \"uk\" => [\n        \"Total Contributions\" => \"Загальний вклад\",\n        \"Current Streak\" => \"Поточна діяльність\",\n        \"Longest Streak\" => \"Найдовша діяльність\",\n        \"Week Streak\" => \"Діяльність за тиждень\",\n        \"Longest Week Streak\" => \"Найбільша к-сть тижнів\",\n        \"Present\" => \"Наразі\",\n        \"Excluding {days}\" => \"Виключаючи {days}\",\n    ],\n    \"ur_PK\" => [\n        \"rtl\" => true,\n        \"Total Contributions\" => \"کل حصہ داری\",\n        \"Current Streak\" => \"موجودہ تسلسل\",\n        \"Longest Streak\" => \"طویل ترین تسلسل\",\n        \"Week Streak\" => \"ہفتہ وار تسلسل\",\n        \"Longest Week Streak\" => \"طویل ترین ہفتہ وار تسلسل\",\n        \"Present\" => \"حاظر\",\n        \"Excluding {days}\" => \"خارج {days}\",\n        \"comma_separator\" => \"، \",\n    ],\n    \"vi\" => [\n        \"Total Contributions\" => \"Tổng số đóng góp\",\n        \"Current Streak\" => \"Chuỗi đóng góp\\nhiện tại\",\n        \"Longest Streak\" => \"Chuỗi đóng góp lớn nhất\",\n        \"Week Streak\" => \"Chuỗi tuần\",\n        \"Longest Week Streak\" => \"Chuỗi tuần lớn nhất\",\n        \"Present\" => \"Hiện tại\",\n        \"Excluding {days}\" => \"Ngoại trừ {days}\",\n    ],\n    \"yo\" => [\n        \"Total Contributions\" => \"Lapapọ ilowosi\",\n        \"Current Streak\" => \"ṣiṣan lọwọlọwọ\",\n        \"Longest Streak\" => \"ṣiṣan ti o gun julọ\",\n        \"Week Streak\" => \"ṣiṣan ọsẹ\",\n        \"Longest Week Streak\" => \"gunjulo ọsẹ ṣiṣan\",\n        \"Present\" => \"lọwọlọwọ\",\n        \"Excluding {days}\" => \"Yato si {days}\",\n    ],\n    \"zh\" => \"zh_Hans\",\n    \"zh_Hans\" => [\n        \"Total Contributions\" => \"合计贡献\",\n        \"Current Streak\" => \"目前连续贡献\",\n        \"Longest Streak\" => \"最长连续贡献\",\n        \"Week Streak\" => \"周连续贡献\",\n        \"Longest Week Streak\" => \"最长周连续贡献\",\n        \"Present\" => \"至今\",\n        \"Excluding {days}\" => \"除外 {days}\",\n        \"comma_separator\" => \"、\",\n    ],\n    \"zh_Hant\" => [\n        \"Total Contributions\" => \"合計貢獻\",\n        \"Current Streak\" => \"目前連續貢獻\",\n        \"Longest Streak\" => \"最長連續貢獻\",\n        \"Week Streak\" => \"周連續貢獻\",\n        \"Longest Week Streak\" => \"最長周連續貢獻\",\n        \"Present\" => \"至今\",\n        \"Excluding {days}\" => \"除外 {days}\",\n        \"comma_separator\" => \"、\",\n    ],\n];\n"
  },
  {
    "path": "src/whitelist.php",
    "content": "<?php\n\n/**\n * Check if a GitHub username is allowed based on the whitelist\n *\n * @param string $user GitHub username to check\n * @return bool True if the username is in the whitelist or if the whitelist is empty, false otherwise\n */\nfunction isWhitelisted(string $user): bool\n{\n    $whitelist = array_map(\"trim\", array_filter(explode(\",\", $_SERVER[\"WHITELIST\"] ?? \"\")));\n    return empty($whitelist) || in_array($user, $whitelist, true);\n}\n"
  },
  {
    "path": "tests/CacheTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PHPUnit\\Framework\\TestCase;\n\n// load functions\nrequire_once dirname(__DIR__, 1) . \"/vendor/autoload.php\";\nrequire_once \"src/cache.php\";\n\nfinal class CacheTest extends TestCase\n{\n    /**\n     * Test cache key generation produces consistent results\n     */\n    public function testCacheKeyConsistency(): void\n    {\n        $key1 = getCacheKey(\"testuser\", [\"mode\" => \"weekly\"]);\n        $key2 = getCacheKey(\"testuser\", [\"mode\" => \"weekly\"]);\n        $this->assertEquals($key1, $key2, \"Same inputs should produce same cache key\");\n    }\n\n    /**\n     * Test cache key generation produces different results for different inputs\n     */\n    public function testCacheKeyDifferentInputs(): void\n    {\n        $key1 = getCacheKey(\"user1\", []);\n        $key2 = getCacheKey(\"user2\", []);\n        $this->assertNotEquals($key1, $key2, \"Different users should produce different cache keys\");\n\n        $key3 = getCacheKey(\"testuser\", [\"mode\" => \"weekly\"]);\n        $key4 = getCacheKey(\"testuser\", [\"mode\" => \"daily\"]);\n        $this->assertNotEquals($key3, $key4, \"Different options should produce different cache keys\");\n    }\n\n    /**\n     * Test that cache key prevents hash collisions\n     * e.g., user \"ab\" with options containing \"cd\" should not collide with user \"abc\" with options containing \"d\"\n     */\n    public function testCacheKeyNoCollisions(): void\n    {\n        // This tests the fix for the hash collision vulnerability\n        $key1 = getCacheKey(\"ab\", [\"option\" => \"cd\"]);\n        $key2 = getCacheKey(\"abc\", [\"option\" => \"d\"]);\n        $this->assertNotEquals($key1, $key2, \"Similar concatenated strings should not produce same hash\");\n\n        $key3 = getCacheKey(\"ab\", [\"x\" => \"cd\"]);\n        $key4 = getCacheKey(\"abcd\", []);\n        $this->assertNotEquals($key3, $key4, \"User + options should not collide with user alone\");\n    }\n\n    /**\n     * Test cache key generation sorts options for consistency\n     */\n    public function testCacheKeyOptionOrdering(): void\n    {\n        $key1 = getCacheKey(\"testuser\", [\"a\" => \"1\", \"b\" => \"2\"]);\n        $key2 = getCacheKey(\"testuser\", [\"b\" => \"2\", \"a\" => \"1\"]);\n        $this->assertEquals($key1, $key2, \"Option order should not affect cache key\");\n    }\n\n    /**\n     * Test cache key is filename-safe (SHA256 hex)\n     */\n    public function testCacheKeyFormat(): void\n    {\n        $key = getCacheKey(\"testuser\", [\"mode\" => \"weekly\"]);\n        $this->assertMatchesRegularExpression(\"/^[a-f0-9]{64}$/\", $key, \"Cache key should be 64-character hex string\");\n    }\n\n    /**\n     * Test cache file path generation\n     */\n    public function testGetCacheFilePath(): void\n    {\n        $key = \"abc123\";\n        $path = getCacheFilePath($key);\n        $this->assertStringEndsWith(\"/cache/abc123.json\", $path);\n    }\n\n    /**\n     * Test setCachedStats and getCachedStats roundtrip\n     */\n    public function testCacheRoundtrip(): void\n    {\n        $user = \"roundtripuser\";\n        $options = [\"mode\" => \"weekly\", \"starting_year\" => 2020];\n        $stats = [\n            \"totalContributions\" => 100,\n            \"currentStreak\" => [\"start\" => \"2024-01-01\", \"end\" => \"2024-01-10\", \"length\" => 10],\n            \"longestStreak\" => [\"start\" => \"2023-06-01\", \"end\" => \"2023-07-15\", \"length\" => 45],\n            \"firstContribution\" => \"2020-01-15\",\n        ];\n\n        // Write to cache\n        $result = setCachedStats($user, $options, $stats);\n        $this->assertTrue($result, \"setCachedStats should return true on success\");\n\n        // Read back from cache\n        $cached = getCachedStats($user, $options);\n        $this->assertNotNull($cached, \"getCachedStats should return cached data\");\n        $this->assertEquals($stats, $cached, \"Cached data should match original\");\n    }\n\n    /**\n     * Test getCachedStats returns null for non-existent cache\n     */\n    public function testGetCachedStatsNotFound(): void\n    {\n        $result = getCachedStats(\"nonexistentuser12345\", []);\n        $this->assertNull($result, \"getCachedStats should return null for non-existent cache\");\n    }\n\n    /**\n     * Test setCachedStats handles invalid data gracefully\n     */\n    public function testSetCachedStatsWithEmptyStats(): void\n    {\n        $result = setCachedStats(\"emptyuser\", [], []);\n        $this->assertTrue($result, \"setCachedStats should handle empty stats array\");\n\n        $cached = getCachedStats(\"emptyuser\", []);\n        $this->assertEquals([], $cached, \"Empty stats should be cached and retrieved\");\n    }\n\n    /**\n     * Test clearUserCache clears cache for user with default options\n     */\n    public function testClearUserCache(): void\n    {\n        $user = \"clearableuser\";\n        $stats = [\"totalContributions\" => 50];\n\n        // Set cache\n        setCachedStats($user, [], $stats);\n        $this->assertNotNull(getCachedStats($user, []));\n\n        // Clear cache\n        $result = clearUserCache($user);\n        $this->assertTrue($result);\n\n        // Verify cleared\n        $this->assertNull(getCachedStats($user, []));\n    }\n\n    /**\n     * Test clearUserCache returns true for non-existent user\n     */\n    public function testClearUserCacheNonExistent(): void\n    {\n        $result = clearUserCache(\"definitelynotauser999\");\n        $this->assertTrue($result, \"clearUserCache should return true for non-existent cache\");\n    }\n\n    /**\n     * Test ensureCacheDir creates directory\n     */\n    public function testEnsureCacheDir(): void\n    {\n        $result = ensureCacheDir();\n        $this->assertTrue($result);\n        $this->assertTrue(is_dir(CACHE_DIR));\n    }\n}\n"
  },
  {
    "path": "tests/OptionsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PHPUnit\\Framework\\TestCase;\n\n// load functions\nrequire_once \"src/card.php\";\n\nfinal class OptionsTest extends TestCase\n{\n    private $defaultTheme = [\n        \"background\" => \"#FFFEFE\",\n        \"border\" => \"#E4E2E2\",\n        \"stroke\" => \"#E4E2E2\",\n        \"ring\" => \"#FB8C00\",\n        \"fire\" => \"#FB8C00\",\n        \"currStreakNum\" => \"#151515\",\n        \"sideNums\" => \"#151515\",\n        \"currStreakLabel\" => \"#FB8C00\",\n        \"sideLabels\" => \"#151515\",\n        \"dates\" => \"#464646\",\n        \"excludeDaysLabel\" => \"#464646\",\n    ];\n\n    /**\n     * Test theme request parameters return colors for theme\n     */\n    public function testThemes(): void\n    {\n        // check that getRequestedTheme returns correct colors for each theme\n        $themes = include \"src/themes.php\";\n        foreach ($themes as $theme => $colors) {\n            $actualColors = getRequestedTheme([\"theme\" => $theme]);\n            $expectedColors = $colors;\n            if (strpos($colors[\"background\"], \",\") !== false) {\n                $expectedColors[\"background\"] = \"url(#gradient)\";\n                // check that the background gradient is correct\n                $this->assertStringContainsString(\"<linearGradient\", $actualColors[\"backgroundGradient\"]);\n            }\n            unset($expectedColors[\"backgroundGradient\"]);\n            unset($actualColors[\"backgroundGradient\"]);\n            $this->assertEquals($expectedColors, $actualColors);\n        }\n    }\n\n    /**\n     * Test that all themes appear in the documentation (docs/themes.md)\n     */\n    public function testThemesInDocumentation(): void\n    {\n        $themes = include \"src/themes.php\";\n        $docContent = file_get_contents(\"docs/themes.md\");\n        foreach (array_keys($themes) as $theme) {\n            $this->assertStringContainsString(\n                \"`$theme`\",\n                $docContent,\n                \"The theme '$theme' is missing from the documentation (docs/themes.md).\",\n            );\n        }\n    }\n\n    /**\n     * Test fallback to default theme\n     */\n    public function testFallbackToDefaultTheme(): void\n    {\n        // check that getRequestedTheme returns default for invalid theme\n        // request parameters\n        $params = [\"theme\" => \"not a theme name\"];\n        // test that invalid theme name gives default values\n        $actual = getRequestedTheme($params);\n        $expected = $this->defaultTheme;\n        $expected[\"backgroundGradient\"] = \"\";\n        $this->assertEquals($expected, $actual);\n    }\n\n    /**\n     * Check that all themes have valid values for all parameters\n     */\n    public function testThemesHaveValidParameters(): void\n    {\n        // check that all themes contain all parameters and have valid values\n        $themes = include \"src/themes.php\";\n        $hexPartialRegex = \"(?:[A-F0-9]{3}|[A-F0-9]{4}|[A-F0-9]{6}|[A-F0-9]{8})\";\n        $hexRegex = \"/^#{$hexPartialRegex}$/\";\n        $backgroundRegex = \"/^#{$hexPartialRegex}|-?\\d+(?:,{$hexPartialRegex})+$/\";\n        foreach ($themes as $theme => $colors) {\n            // check that there are no extra keys in the theme\n            $this->assertEquals(\n                array_diff_key($colors, $this->defaultTheme),\n                [],\n                \"The theme '$theme' contains invalid parameters.\",\n            );\n            # check that no parameters are missing and all values are valid\n            foreach (array_keys($this->defaultTheme) as $param) {\n                // check that the key exists\n                $this->assertArrayHasKey($param, $colors, \"The theme '$theme' is missing the key '$param'.\");\n                if ($param === \"background\") {\n                    // check that the key is a valid background value\n                    $this->assertMatchesRegularExpression(\n                        $backgroundRegex,\n                        $colors[$param],\n                        \"The parameter '$param' of '$theme' is not a valid background value.\",\n                    );\n                    continue;\n                }\n                // check that the key is a valid hex color\n                $this->assertMatchesRegularExpression(\n                    $hexRegex,\n                    strtoupper($colors[$param]),\n                    \"The parameter '$param' of '$theme' is not a valid hex color.\",\n                );\n                // check that the key is a valid hex color in uppercase\n                $this->assertMatchesRegularExpression(\n                    $hexRegex,\n                    $colors[$param],\n                    \"The parameter '$param' of '$theme' should not contain lowercase letters.\",\n                );\n            }\n        }\n    }\n\n    /**\n     * Test parameters to override specific color\n     */\n    public function testColorOverrideParameters(): void\n    {\n        // clear request parameters\n        $params = [];\n        // set default expected value\n        $expected = $this->defaultTheme;\n        foreach (array_keys($this->defaultTheme) as $param) {\n            // set request parameter\n            $params[$param] = \"f00\";\n            // update parameter in expected result\n            $expected = array_merge($expected, [$param => \"#f00\"]);\n            // test color change\n            $actual = getRequestedTheme($params);\n            $expected[\"backgroundGradient\"] = \"\";\n            $this->assertEquals($expected, $actual);\n        }\n    }\n\n    /**\n     * Test color override parameters - all valid color inputs\n     */\n    public function testValidColorInputs(): void\n    {\n        // valid color inputs and what the output color will be\n        $validInputTypes = [\n            \"f00\" => \"#f00\",\n            \"f00f\" => \"#f00f\",\n            \"ff0000\" => \"#ff0000\",\n            \"FF0000\" => \"#ff0000\",\n            \"ff0000ff\" => \"#ff0000ff\",\n            \"red\" => \"red\",\n        ];\n        // set default expected value\n        $expected = $this->defaultTheme;\n        foreach ($validInputTypes as $input => $output) {\n            // set request parameter\n            $params = [\"background\" => $input];\n            // update parameter in expected result\n            $expected = array_merge($expected, [\"background\" => $output]);\n            // test color change\n            $actual = getRequestedTheme($params);\n            $expected[\"backgroundGradient\"] = \"\";\n            $this->assertEquals($expected, $actual);\n        }\n    }\n\n    /**\n     * Test color override parameters - invalid color inputs\n     */\n    public function testInvalidColorInputs(): void\n    {\n        // invalid color inputs\n        $invalidInputTypes = [\n            \"g00\", # not 0-9, A-F\n            \"f00f0\", # invalid number of characters\n            \"fakecolor\", # invalid color name\n        ];\n        foreach ($invalidInputTypes as $input) {\n            // set request parameter\n            $params = [\"background\" => $input];\n            // test that theme is still default\n            $actual = getRequestedTheme($params);\n            $expected = $this->defaultTheme;\n            $expected[\"backgroundGradient\"] = \"\";\n            $this->assertEquals($expected, $actual);\n        }\n    }\n\n    /**\n     * Test hide_border parameter\n     */\n    public function testHideBorder(): void\n    {\n        // check that getRequestedTheme returns transparent border when hide_border is true\n        $params = [\"hide_border\" => \"true\"];\n        $theme = getRequestedTheme($params);\n        $this->assertEquals(\"#0000\", $theme[\"border\"]);\n        // check that getRequestedTheme returns solid border when hide_border is not true\n        $params = [\"hide_border\" => \"false\"];\n        $theme = getRequestedTheme($params);\n        $this->assertEquals($this->defaultTheme[\"border\"], $theme[\"border\"]);\n    }\n\n    /**\n     * Test date formatter for same year\n     */\n    public function testDateFormatSameYear(): void\n    {\n        $year = date(\"Y\");\n        $formatted = formatDate(\"$year-04-12\", \"M j[, Y]\", \"en\");\n        $this->assertEquals(\"Apr 12\", $formatted);\n    }\n\n    /**\n     * Test date formatter for different year\n     */\n    public function testDateFormatDifferentYear(): void\n    {\n        $formatted = formatDate(\"2000-04-12\", \"M j[, Y]\", \"en\");\n        $this->assertEquals(\"Apr 12, 2000\", $formatted);\n    }\n\n    /**\n     * Test date formatter no brackets different year\n     */\n    public function testDateFormatNoBracketsDiffYear(): void\n    {\n        $formatted = formatDate(\"2000-04-12\", \"Y/m/d\", \"en\");\n        $this->assertEquals(\"2000/04/12\", $formatted);\n    }\n\n    /**\n     * Test date formatter no brackets same year\n     */\n    public function testDateFormatNoBracketsSameYear(): void\n    {\n        $year = date(\"Y\");\n        $formatted = formatDate(\"$year-04-12\", \"Y/m/d\", \"en\");\n        $this->assertEquals(\"$year/04/12\", $formatted);\n    }\n\n    /**\n     * Test normalizing theme name\n     */\n    public function testNormalizeThemeName(): void\n    {\n        $this->assertEquals(\"mytheme\", normalizeThemeName(\"myTheme\"));\n        $this->assertEquals(\"my-theme\", normalizeThemeName(\"My_Theme\"));\n        $this->assertEquals(\"my-theme\", normalizeThemeName(\"my_theme\"));\n        $this->assertEquals(\"my-theme\", normalizeThemeName(\"my-theme\"));\n    }\n\n    /**\n     * Test all theme names are normalized\n     */\n    public function testAllThemeNamesNormalized(): void\n    {\n        $themes = include \"src/themes.php\";\n        foreach (array_keys($themes) as $theme) {\n            $normalized = normalizeThemeName($theme);\n            $this->assertEquals(\n                $theme,\n                $normalized,\n                \"Theme name '$theme' is not normalized. It should contain only lowercase letters, numbers, and dashes. Consider renaming it to '$normalized'.\",\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "tests/RenderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PHPUnit\\Framework\\TestCase;\n\n// load functions\nrequire_once \"src/card.php\";\n\nfinal class RenderTest extends TestCase\n{\n    private $testParams = [\n        \"background\" => \"000000\",\n        \"border\" => \"111111\",\n        \"stroke\" => \"222222\",\n        \"ring\" => \"333333\",\n        \"fire\" => \"444444\",\n        \"currStreakNum\" => \"555555\",\n        \"sideNums\" => \"666666\",\n        \"currStreakLabel\" => \"777777\",\n        \"sideLabels\" => \"888888\",\n        \"dates\" => \"999999\",\n        \"excludeDaysLabel\" => \"aaaaaa\",\n    ];\n\n    private $testStats = [\n        \"mode\" => \"daily\",\n        \"totalContributions\" => 2048,\n        \"firstContribution\" => \"2016-08-10\",\n        \"longestStreak\" => [\n            \"start\" => \"2016-12-19\",\n            \"end\" => \"2016-03-14\",\n            \"length\" => 86,\n        ],\n        \"currentStreak\" => [\n            \"start\" => \"2019-03-28\",\n            \"end\" => \"2019-04-12\",\n            \"length\" => 16,\n        ],\n        \"excludedDays\" => [],\n    ];\n\n    /**\n     * Test normal card render\n     */\n    public function testCardRender(): void\n    {\n        // Check that the card is rendered as expected\n        $render = generateCard($this->testStats, $this->testParams);\n        $expected = file_get_contents(\"tests/expected/test_card.svg\");\n        $this->assertEquals($expected, $render);\n\n        // Test short_numbers parameter\n        $this->testParams[\"short_numbers\"] = \"true\";\n        $render = generateCard($this->testStats, $this->testParams);\n        $this->assertStringContainsString(\"2K\", $render);\n    }\n\n    /**\n     * Test error card render\n     */\n    public function testErrorCardRender(): void\n    {\n        // Check that error card is returned when no stats are provided\n        $render = generateErrorCard(\"An unknown error occurred\", $this->testParams);\n        $expected = file_get_contents(\"tests/expected/test_error_card.svg\");\n        $this->assertEquals($expected, $render);\n    }\n\n    /**\n     * Test date_format parameter in render\n     */\n    public function testDateFormatRender(): void\n    {\n        $year = date(\"Y\");\n        $this->testStats[\"currentStreak\"][\"end\"] = \"$year-04-12\";\n        $this->testParams[\"date_format\"] = \"[Y-]m-d\";\n        // Check that the card is rendered as expected\n        $render = generateCard($this->testStats, $this->testParams);\n        $this->assertStringContainsString(\"2016-08-10 - Present\", $render);\n        $this->assertStringContainsString(\"2019-03-28 - 04-12\", $render);\n        $this->assertStringContainsString(\"2016-12-19 - 2016-03-14\", $render);\n    }\n\n    /**\n     * Test locale parameter in render with date_format in translation file\n     */\n    public function testLocaleRenderDateFormat(): void\n    {\n        $this->testParams[\"locale\"] = \"ja\";\n        // Check that the card is rendered as expected\n        $render = generateCard($this->testStats, $this->testParams);\n        $this->assertStringContainsString(\"2,048\", $render);\n        $this->assertStringContainsString(\"総ｺﾝﾄﾘﾋﾞｭｰｼｮﾝ数\", $render);\n        $this->assertStringContainsString(\"2016.8.10 - 今\", $render);\n        $this->assertStringContainsString(\"16\", $render);\n        $this->assertStringContainsString(\"現在のストリーク\", $render);\n        $this->assertStringContainsString(\"2019.3.28 - 2019.4.12\", $render);\n        $this->assertStringContainsString(\"86\", $render);\n        $this->assertStringContainsString(\"最長のストリーク\", $render);\n        $this->assertStringContainsString(\"2016.12.19 - 2016.3.14\", $render);\n    }\n\n    /**\n     * Test border radius\n     */\n    public function testBorderRadius(): void\n    {\n        $this->testParams[\"border_radius\"] = \"16\";\n        // Check that the card is rendered as expected\n        $render = generateCard($this->testStats, $this->testParams);\n        $this->assertStringContainsString(\"<rect width='495' height='195' rx='16'/>\", $render);\n        $this->assertStringContainsString(\n            \"<rect stroke='#111111' fill='#000000' rx='16' x='0.5' y='0.5' width='494' height='194'/>\",\n            $render,\n        );\n    }\n\n    /**\n     * Test split lines function\n     */\n    public function testSplitLines(): void\n    {\n        // Check normal label, no split\n        $this->assertEquals(\"Total Contributions\", splitLines(\"Total Contributions\", 24, -9));\n        // Check label that is too long, split\n        $this->assertEquals(\n            \"<tspan x='0' dy='-9'>Chuỗi đóng góp hiện</tspan><tspan x='0' dy='16'>tại</tspan>\",\n            splitLines(\"Chuỗi đóng góp hiện tại\", 22, -9),\n        );\n        // Check label with manually inserted line break, split\n        $this->assertEquals(\n            \"<tspan x='0' dy='-9'>Chuỗi đóng góp</tspan><tspan x='0' dy='16'>hiện tại</tspan>\",\n            splitLines(\"Chuỗi đóng góp\\nhiện tại\", 22, -9),\n        );\n        // Check date range label, no split\n        $this->assertEquals(\"Mar 28, 2019 – Apr 12, 2019\", splitLines(\"Mar 28, 2019 – Apr 12, 2019\", 28, 0));\n        // Check date range label that is too long, split\n        $this->assertEquals(\n            \"<tspan x='0' dy='0'>19 de dez. de 2021</tspan><tspan x='0' dy='16'>- 14 de mar.</tspan>\",\n            splitLines(\"19 de dez. de 2021 - 14 de mar.\", 24, 0),\n        );\n    }\n\n    /**\n     * Test disable_animations parameter\n     */\n    public function testDisableAnimations(): void\n    {\n        $this->testParams[\"disable_animations\"] = \"true\";\n        // Check that the card is rendered as expected\n        $response = generateOutput($this->testStats, $this->testParams);\n        $render = $response[\"body\"];\n        $this->assertStringNotContainsString(\"opacity: 0;\", $render);\n        $this->assertStringContainsString(\"opacity: 1;\", $render);\n        $this->assertStringContainsString(\"font-size: 28px;\", $render);\n        $this->assertStringNotContainsString(\"animation:\", $render);\n        $this->assertStringNotContainsString(\"<style>\", $render);\n    }\n\n    /**\n     * Test alpha in hex colors\n     */\n    public function testAlphaInHexColors(): void\n    {\n        // \"tranparent\" gets converted to \"#0000\"\n        $this->testParams[\"background\"] = \"transparent\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='#000000' fill-opacity='0'\", $render);\n\n        // \"#ff000080\" gets converted to \"#ff0000\" and fill-opacity is set to 0.50196078431373\n        $this->testParams[\"background\"] = \"ff000080\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='#ff0000' fill-opacity='0.50196078431373'\", $render);\n\n        // \"#ff0000\" gets converted to \"#ff0000\" and fill-opacity is not set\n        $this->testParams[\"background\"] = \"ff0000ff\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='#ff0000' fill-opacity='1'\", $render);\n\n        // test stroke opacity\n        $this->testParams[\"border\"] = \"00ff0080\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"stroke='#00ff00' stroke-opacity='0.50196078431373'\", $render);\n    }\n\n    /**\n     * Test gradient background\n     */\n    public function testGradientBackground(): void\n    {\n        $this->testParams[\"background\"] = \"45,f00,e11\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='url(#gradient)'\", $render);\n        $this->assertStringContainsString(\n            \"<linearGradient id='gradient' gradientTransform='rotate(45)' gradientUnits='userSpaceOnUse'><stop offset='0%' stop-color='#f00' /><stop offset='100%' stop-color='#e11' /></linearGradient>\",\n            $render,\n        );\n    }\n\n    /**\n     * Test gradient background with more than 2 colors\n     */\n    public function testGradientBackgroundWithMoreThan2Colors(): void\n    {\n        $this->testParams[\"background\"] = \"-45,f00,4e5,ddd,fff\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='url(#gradient)'\", $render);\n        $this->assertStringContainsString(\n            \"<linearGradient id='gradient' gradientTransform='rotate(-45)' gradientUnits='userSpaceOnUse'><stop offset='0%' stop-color='#f00' /><stop offset='33.333333333333%' stop-color='#4e5' /><stop offset='66.666666666667%' stop-color='#ddd' /><stop offset='100%' stop-color='#fff' /></linearGradient>\",\n            $render,\n        );\n    }\n\n    /**\n     * Test excluding days\n     */\n    public function testExcludeDays(): void\n    {\n        $this->testStats[\"excludedDays\"] = [\"Sun\", \"Sat\"];\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"* Excluding Sun, Sat\", $render);\n    }\n\n    /**\n     * Test card width option\n     */\n    public function testCardWidth(): void\n    {\n        $this->testParams[\"card_width\"] = \"600\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"viewBox='0 0 600 195' width='600px' height='195px'\", $render);\n        $this->assertStringContainsString(\"<rect width='600' height='195' rx='4.5'/>\", $render);\n        $this->assertStringContainsString(\"<line x1='400' y1='28'\", $render);\n        $this->assertStringContainsString(\"<line x1='200' y1='28'\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(100,\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(300,\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(500,\", $render);\n    }\n\n    /**\n     * Test card height option\n     */\n    public function testCardHeight(): void\n    {\n        $this->testParams[\"card_height\"] = \"300\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"viewBox='0 0 495 300' width='495px' height='300px'\", $render);\n        $this->assertStringContainsString(\"<rect width='495' height='300' rx='4.5'/>\", $render);\n        $this->assertStringContainsString(\"<line x1='165' y1='54.25'\", $render);\n        $this->assertStringContainsString(\"<line x1='330' y1='54.25'\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(82.5, 100.5)'>\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(247.5, 100.5)'>\", $render);\n        $this->assertStringContainsString(\"<g transform='translate(412.5, 100.5)'>\", $render);\n    }\n\n    /**\n     * Test first and third columns swapped when direction is rtl\n     */\n    public function testFirstAndThirdColumnsSwappedWhenDirectionIsRtl(): void\n    {\n        $this->testParams[\"locale\"] = \"he\";\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertMatchesRegularExpression(\n            \"/<!-- Total Contributions big number -->\\\\s*<g transform='translate\\\\(412\\\\.5, 48\\\\)'>/\",\n            $render,\n        );\n        $this->assertMatchesRegularExpression(\n            \"/<!-- Longest Streak big number -->\\\\s*<g transform='translate\\\\(82\\\\.5, 48\\\\)'>/\",\n            $render,\n        );\n    }\n\n    /**\n     * Test excluded days of the week\n     */\n    public function testExcludeDaysParameter(): void\n    {\n        $this->testParams[\"exclude_days\"] = \"Sun,Sat\";\n        $this->testStats[\"excludedDays\"] = [\"Sun\", \"Sat\"];\n        $render = generateOutput($this->testStats, $this->testParams)[\"body\"];\n        $this->assertStringContainsString(\"fill='#aaaaaa'\", $render);\n        $this->assertStringContainsString(\"* Excluding Sun, Sat\", $render);\n    }\n}\n"
  },
  {
    "path": "tests/StatsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PHPUnit\\Framework\\TestCase;\n\n// load functions\nrequire_once dirname(__DIR__, 1) . \"/vendor/autoload.php\";\nrequire_once \"src/stats.php\";\n\n// load .env\n$dotenv = \\Dotenv\\Dotenv::createImmutable(dirname(__DIR__, 1));\n$dotenv->safeLoad();\n\n// if environment variables are not loaded, display error\nif (!isset($_SERVER[\"TOKEN\"])) {\n    $message = file_exists(dirname(__DIR__ . \"../.env\", 1))\n        ? \"Missing token in config. Check Contributing.md for details.\"\n        : \".env was not found. Check Contributing.md for details.\";\n\n    die($message);\n}\n\nfinal class StatsTest extends TestCase\n{\n    /**\n     * Test that values seem correct for valid username\n     */\n    public function testValidUsername(): void\n    {\n        $contributionGraphs = getContributionGraphs(\"DenverCoder1\");\n        $contributions = getContributionDates($contributionGraphs);\n        $stats = getContributionStats($contributions);\n        // test total contributions\n        $this->assertIsInt($stats[\"totalContributions\"]);\n        $this->assertGreaterThan(2300, $stats[\"totalContributions\"]);\n        // test first contribution\n        $this->assertEquals(\"2016-08-10\", $stats[\"firstContribution\"]);\n        // test longest streak length\n        $this->assertIsInt($stats[\"longestStreak\"][\"length\"]);\n        $this->assertGreaterThanOrEqual(98, $stats[\"longestStreak\"][\"length\"]);\n        // test current streak length\n        $this->assertIsInt($stats[\"currentStreak\"][\"length\"]);\n        $this->assertGreaterThanOrEqual(0, $stats[\"currentStreak\"][\"length\"]);\n        // test longest streak start date are in form YYYY-MM-DD\n        $this->assertMatchesRegularExpression(\"/2\\d{3}-[01]\\d-[0-3]\\d/\", $stats[\"longestStreak\"][\"start\"]);\n        // test longest streak end date are in form YYYY-MM-DD\n        $this->assertMatchesRegularExpression(\"/2\\d{3}-[01]\\d-[0-3]\\d/\", $stats[\"longestStreak\"][\"end\"]);\n        // test current streak start date are in form YYYY-MM-DD\n        $this->assertMatchesRegularExpression(\"/2\\d{3}-[01]\\d-[0-3]\\d/\", $stats[\"currentStreak\"][\"start\"]);\n        // test current streak end date are in form YYYY-MM-DD\n        $this->assertMatchesRegularExpression(\"/2\\d{3}-[01]\\d-[0-3]\\d/\", $stats[\"currentStreak\"][\"end\"]);\n        // test current streak end date is today or yesterday\n        $this->assertContains($stats[\"currentStreak\"][\"end\"], [\n            date(\"Y-m-d\"),\n            date(\"Y-m-d\", strtotime(\"yesterday\")),\n            date(\"Y-m-d\", strtotime(\"tomorrow\")),\n        ]);\n        // test length of longest streak matches time between start and end dates\n        $longestStreakDelta = strtotime($stats[\"longestStreak\"][\"end\"]) - strtotime($stats[\"longestStreak\"][\"start\"]);\n        $this->assertEquals($longestStreakDelta / 60 / 60 / 24 + 1, $stats[\"longestStreak\"][\"length\"]);\n        // if the current streak is 0, the start date should be the same as the end date\n        if ($stats[\"currentStreak\"][\"length\"] == 0) {\n            $this->assertEquals($stats[\"currentStreak\"][\"start\"], $stats[\"currentStreak\"][\"end\"]);\n        }\n        // test length of current streak matches time between start and end dates\n        else {\n            $currentStreakDelta =\n                strtotime($stats[\"currentStreak\"][\"end\"]) - strtotime($stats[\"currentStreak\"][\"start\"]);\n            $this->assertEquals($currentStreakDelta / 60 / 60 / 24 + 1, $stats[\"currentStreak\"][\"length\"]);\n        }\n    }\n\n    /**\n     * Test contributions with overriden starting year\n     */\n    public function testOverrideStartingYear(): void\n    {\n        $contributionGraphs = getContributionGraphs(\"DenverCoder1\", 2019);\n        $contributions = getContributionDates($contributionGraphs);\n        $stats = getContributionStats($contributions);\n        // test first contribution\n        $this->assertEquals(\"2019-01-01\", $stats[\"firstContribution\"]);\n    }\n\n    /**\n     * Test that an invalid username returns 'not found' error\n     */\n    public function testInvalidUsername(): void\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage(\"Could not find a user with that name.\");\n        getContributionGraphs(\"help\");\n    }\n\n    /**\n     * Test that an valid username can be accessed with whitelist\n     */\n    public function testValidUsernameWithWhitelist(): void\n    {\n        $_SERVER[\"WHITELIST\"] = \"DenverCoder1\";\n        try {\n            $contributionGraphs = getContributionGraphs(\"DenverCoder1\");\n            $this->assertIsArray($contributionGraphs);\n            $this->assertNotEmpty($contributionGraphs);\n        } finally {\n            unset($_SERVER[\"WHITELIST\"]);\n        }\n    }\n\n    /**\n     * Test that an not whitelisted username returns 'not whitelisted' error\n     */\n    public function testNotWhitelistedUsername(): void\n    {\n        $_SERVER[\"WHITELIST\"] = \"DenverCoder1\";\n        try {\n            $this->expectException(InvalidArgumentException::class);\n            $this->expectExceptionMessage(\"User not in whitelist.\");\n            getContributionGraphs(\"help\");\n        } finally {\n            unset($_SERVER[\"WHITELIST\"]);\n        }\n    }\n\n    /**\n     * Test that an organization name returns 'not a user' error\n     */\n    public function testOrganizationName(): void\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage(\"Could not find a user with that name.\");\n        getContributionGraphs(\"DenverCoderOne\");\n    }\n\n    /**\n     * Test stats contributed today\n     */\n    public function testContributedToday(): void\n    {\n        $contributions = [\n            \"2021-04-15\" => 5,\n            \"2021-04-16\" => 3,\n            \"2021-04-17\" => 2,\n            \"2021-04-18\" => 7,\n        ];\n        $stats = getContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 17,\n            \"firstContribution\" => \"2021-04-15\",\n            \"longestStreak\" => [\n                \"start\" => \"2021-04-15\",\n                \"end\" => \"2021-04-18\",\n                \"length\" => 4,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2021-04-15\",\n                \"end\" => \"2021-04-18\",\n                \"length\" => 4,\n            ],\n            \"excludedDays\" => [],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test stats missing today\n     */\n    public function testMissingToday(): void\n    {\n        $contributions = [\n            \"2021-04-15\" => 5,\n            \"2021-04-16\" => 3,\n            \"2021-04-17\" => 2,\n            \"2021-04-18\" => 0,\n        ];\n        $stats = getContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 10,\n            \"firstContribution\" => \"2021-04-15\",\n            \"longestStreak\" => [\n                \"start\" => \"2021-04-15\",\n                \"end\" => \"2021-04-17\",\n                \"length\" => 3,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2021-04-15\",\n                \"end\" => \"2021-04-17\",\n                \"length\" => 3,\n            ],\n            \"excludedDays\" => [],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test stats missing 2 days\n     */\n    public function testMissingTwoDays(): void\n    {\n        $contributions = [\n            \"2021-04-15\" => 5,\n            \"2021-04-16\" => 3,\n            \"2021-04-17\" => 0,\n            \"2021-04-18\" => 0,\n        ];\n        $stats = getContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 8,\n            \"firstContribution\" => \"2021-04-15\",\n            \"longestStreak\" => [\n                \"start\" => \"2021-04-15\",\n                \"end\" => \"2021-04-16\",\n                \"length\" => 2,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2021-04-18\",\n                \"end\" => \"2021-04-18\",\n                \"length\" => 0,\n            ],\n            \"excludedDays\" => [],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test multiple year streak\n     */\n    public function testMultipleYearStreak(): void\n    {\n        $contributions = [];\n        for ($i = 369; $i >= 0; --$i) {\n            $contributions[date(\"Y-m-d\", strtotime(\"$i days ago\"))] = 1;\n        }\n        $stats = getContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 370,\n            \"firstContribution\" => date(\"Y-m-d\", strtotime(\"369 days ago\")),\n            \"longestStreak\" => [\n                \"start\" => date(\"Y-m-d\", strtotime(\"369 days ago\")),\n                \"end\" => date(\"Y-m-d\"),\n                \"length\" => 370,\n            ],\n            \"currentStreak\" => [\n                \"start\" => date(\"Y-m-d\", strtotime(\"369 days ago\")),\n                \"end\" => date(\"Y-m-d\"),\n                \"length\" => 370,\n            ],\n            \"excludedDays\" => [],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test future commits\n     * Tomorrow should count because of timezone differences, but further ahead should not\n     */\n    public function testFutureCommits(): void\n    {\n        $yesterday = date(\"Y-m-d\", strtotime(\"yesterday\"));\n        $today = date(\"Y-m-d\", strtotime(\"today\"));\n        $tomorrow = date(\"Y-m-d\", strtotime(\"tomorrow\"));\n        $inTwoDays = date(\"Y-m-d\", strtotime(\"$today +2 days\"));\n        $contributionGraphs = [\n            (object) [\n                \"data\" => (object) [\n                    \"user\" => (object) [\n                        \"contributionsCollection\" => (object) [\n                            \"contributionCalendar\" => (object) [\n                                \"weeks\" => (object) [\n                                    (object) [\n                                        \"contributionDays\" => (object) [\n                                            (object) [\n                                                \"contributionCount\" => 1,\n                                                \"date\" => $yesterday,\n                                            ],\n                                            (object) [\n                                                \"contributionCount\" => 1,\n                                                \"date\" => $today,\n                                            ],\n                                            (object) [\n                                                \"contributionCount\" => 1,\n                                                \"date\" => $tomorrow,\n                                            ],\n                                            (object) [\n                                                \"contributionCount\" => 1,\n                                                \"date\" => $inTwoDays,\n                                            ],\n                                        ],\n                                    ],\n                                ],\n                            ],\n                        ],\n                    ],\n                ],\n            ],\n        ];\n        $contributions = getContributionDates($contributionGraphs);\n        $stats = getContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 3,\n            \"firstContribution\" => date(\"Y-m-d\", strtotime(\"yesterday\")),\n            \"longestStreak\" => [\n                \"start\" => date(\"Y-m-d\", strtotime(\"yesterday\")),\n                \"end\" => date(\"Y-m-d\", strtotime(\"tomorrow\")),\n                \"length\" => 3,\n            ],\n            \"currentStreak\" => [\n                \"start\" => date(\"Y-m-d\", strtotime(\"yesterday\")),\n                \"end\" => date(\"Y-m-d\", strtotime(\"tomorrow\")),\n                \"length\" => 3,\n            ],\n            \"excludedDays\" => [],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test weekly stats\n     */\n    public function testWeeklyStats(): void\n    {\n        $contributions = [\n            \"2022-11-12\" => 5,\n            \"2022-11-13\" => 3, // Sunday\n            \"2022-11-14\" => 2,\n            \"2022-11-15\" => 0,\n            \"2022-11-16\" => 0,\n            \"2022-11-17\" => 0,\n            \"2022-11-18\" => 0,\n            \"2022-11-19\" => 0,\n            \"2022-11-20\" => 0, // Sunday\n            \"2022-11-21\" => 1,\n        ];\n        $stats = getWeeklyContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"weekly\",\n            \"totalContributions\" => 11,\n            \"firstContribution\" => \"2022-11-12\",\n            \"longestStreak\" => [\n                \"start\" => \"2022-11-06\", // Previous Sunday before 2022-11-12\n                \"end\" => \"2022-11-20\",\n                \"length\" => 3,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2022-11-06\",\n                \"end\" => \"2022-11-20\",\n                \"length\" => 3,\n            ],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test weekly stats missing a week\n     */\n    public function testWeeklyStatsMissingWeek(): void\n    {\n        $contributions = [\n            \"2022-11-05\" => 2,\n            \"2022-11-06\" => 0, // Sunday\n            \"2022-11-07\" => 0,\n            \"2022-11-08\" => 0,\n            \"2022-11-09\" => 0,\n            \"2022-11-10\" => 0,\n            \"2022-11-11\" => 0,\n            \"2022-11-12\" => 5,\n            \"2022-11-13\" => 0, // Sunday\n            \"2022-11-14\" => 0,\n            \"2022-11-15\" => 0,\n            \"2022-11-16\" => 0,\n            \"2022-11-17\" => 0,\n            \"2022-11-18\" => 0,\n            \"2022-11-19\" => 0,\n            \"2022-11-20\" => 0, // Sunday\n            \"2022-11-21\" => 1,\n            \"2022-11-22\" => 1,\n        ];\n        $stats = getWeeklyContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"weekly\",\n            \"totalContributions\" => 9,\n            \"firstContribution\" => \"2022-11-05\",\n            \"longestStreak\" => [\n                \"start\" => \"2022-10-30\", // Previous Sunday before 2022-11-05\n                \"end\" => \"2022-11-06\",\n                \"length\" => 2,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2022-11-20\",\n                \"end\" => \"2022-11-20\",\n                \"length\" => 1,\n            ],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test weekly stats missing this week\n     */\n    public function testWeeklyStatsMissingThisWeek(): void\n    {\n        $contributions = [];\n        $thisWeek = getPreviousSunday(date(\"Y-m-d\"));\n        $lastWeek = getPreviousSunday(date(\"Y-m-d\", strtotime(\"$thisWeek -1 week\")));\n        for ($i = 0; $i < 7; $i++) {\n            $date = date(\"Y-m-d\", strtotime(\"$lastWeek +$i days\"));\n            $contributions[$date] = 1;\n        }\n        for ($i = 0; $i < 7; $i++) {\n            $date = date(\"Y-m-d\", strtotime(\"$thisWeek +$i days\"));\n            $contributions[$date] = 0;\n        }\n        $stats = getWeeklyContributionStats($contributions);\n        $expected = [\n            \"mode\" => \"weekly\",\n            \"totalContributions\" => 7,\n            \"firstContribution\" => $lastWeek,\n            \"longestStreak\" => [\n                \"start\" => $lastWeek,\n                \"end\" => $lastWeek,\n                \"length\" => 1,\n            ],\n            \"currentStreak\" => [\n                \"start\" => $lastWeek,\n                \"end\" => $lastWeek,\n                \"length\" => 1,\n            ],\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test stats with excluded days of the week\n     */\n    public function testExcludeDays(): void\n    {\n        $contributions = [\n            \"2023-04-12\" => 1,\n            \"2023-04-13\" => 0,\n            \"2023-04-14\" => 2,\n            \"2023-04-15\" => 0,\n            \"2023-04-16\" => 0,\n            \"2023-04-17\" => 3,\n        ];\n        $excludeDays = [\"Sun\", \"Sat\"];\n        $stats = getContributionStats($contributions, $excludeDays);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 6,\n            \"firstContribution\" => \"2023-04-12\",\n            \"longestStreak\" => [\n                \"start\" => \"2023-04-14\",\n                \"end\" => \"2023-04-17\",\n                \"length\" => 4,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2023-04-14\",\n                \"end\" => \"2023-04-17\",\n                \"length\" => 4,\n            ],\n            \"excludedDays\" => $excludeDays,\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n\n    /**\n     * Test stats with excluded days of the week and no contribution before weekend\n     */\n    public function testExcludeDaysNoContributionBeforeWeekend(): void\n    {\n        $contributions = [\n            \"2023-04-12\" => 1,\n            \"2023-04-13\" => 2,\n            \"2023-04-14\" => 0,\n            \"2023-04-15\" => 0,\n            \"2023-04-16\" => 0,\n            \"2023-04-17\" => 3,\n        ];\n        $excludeDays = [\"Sun\", \"Sat\"];\n        $stats = getContributionStats($contributions, $excludeDays);\n        $expected = [\n            \"mode\" => \"daily\",\n            \"totalContributions\" => 6,\n            \"firstContribution\" => \"2023-04-12\",\n            \"longestStreak\" => [\n                \"start\" => \"2023-04-12\",\n                \"end\" => \"2023-04-13\",\n                \"length\" => 2,\n            ],\n            \"currentStreak\" => [\n                \"start\" => \"2023-04-17\",\n                \"end\" => \"2023-04-17\",\n                \"length\" => 1,\n            ],\n            \"excludedDays\" => $excludeDays,\n        ];\n        $this->assertEquals($expected, $stats);\n    }\n}\n"
  },
  {
    "path": "tests/TranslationsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PHPUnit\\Framework\\TestCase;\n\n// load functions\nrequire_once dirname(__DIR__, 1) . \"/vendor/autoload.php\";\n\nfinal class TranslationsTest extends TestCase\n{\n    /**\n     * Test that all locales only contain phrases appearing for the locale \"en\" or \"date_format\"\n     */\n    public function testAllPhrasesValid(): void\n    {\n        $translations = include \"src/translations.php\";\n        $locales = array_keys($translations);\n        $valid_phrases = [\n            \"rtl\",\n            \"date_format\",\n            \"Total Contributions\",\n            \"Current Streak\",\n            \"Longest Streak\",\n            \"Week Streak\",\n            \"Longest Week Streak\",\n            \"Present\",\n            \"Excluding {days}\",\n            \"comma_separator\",\n        ];\n        foreach ($locales as $locale) {\n            // if it is a string, assert that the alias exists in the translations file\n            if (is_string($translations[$locale])) {\n                $alias = $translations[$locale];\n                $this->assertArrayHasKey($alias, $translations, \"Alias '{$alias}' does not exist for locale '$locale'\");\n            }\n            // otherwise, assert that all phrases are valid\n            else {\n                $phrases = array_keys($translations[$locale]);\n                $this->assertEmpty(array_diff($phrases, $valid_phrases), \"Locale '$locale' contains invalid phrases\");\n            }\n        }\n    }\n\n    /**\n     * Test that \"en\" is first and the remaining locales are sorted alphabetically\n     */\n    public function testLocalesSortedAlphabetically(): void\n    {\n        $translations = include \"src/translations.php\";\n        $locales = array_keys($translations);\n        // check that \"en\" is first\n        $this->assertEquals(\"en\", $locales[0]);\n        // check that the remaining locales are sorted alphabetically\n        $remaining_locales = array_slice($locales, 1);\n        $sorted_locales = call_user_func(function (array $arr) {\n            asort($arr);\n            return $arr;\n        }, $remaining_locales);\n        // check that the remaining locales are sorted alphabetically\n        $sorted = implode(\", \", $sorted_locales);\n        $remaining = implode(\", \", $remaining_locales);\n        $this->assertEquals($sorted, $remaining, \"Locales are not sorted alphabetically\");\n    }\n\n    /**\n     * Test that all keys are normalized - lowercase language code, title case script code, uppercase region code\n     */\n    public function testKeysNormalized(): void\n    {\n        $translations = include \"src/translations.php\";\n        $locales = array_keys($translations);\n        foreach ($locales as $locale) {\n            // normalize locale code\n            $normalized = normalizeLocaleCode($locale);\n            // check that the locale is normalized\n            $this->assertEquals($locale, $normalized, \"Locale '$locale' is not normalized. Expected '$normalized'.\");\n        }\n    }\n\n    /**\n     * Test getTranslations() function\n     */\n    public function testGetTranslations(): void\n    {\n        $translations = include \"src/translations.php\";\n        $en = $translations[\"en\"];\n        // test alias\n        $this->assertEquals($translations[\"zh_Hans\"] + $en, getTranslations(\"zh\"), \"Alias not resolved\");\n        // test locale not normalized with script\n        $this->assertEquals($translations[\"zh_Hans\"] + $en, getTranslations(\"zh-hans\"), \"Locale script not normalized\");\n        // test locale not normalized with region\n        $this->assertEquals($translations[\"pt_BR\"] + $en, getTranslations(\"pt-br\"), \"Locale region not normalized\");\n        // test locale region not in translations, but language is\n        $this->assertEquals($translations[\"fr\"] + $en, getTranslations(\"fr_XX\"), \"Locale missing region not resolved\");\n        // test locale not found\n        $this->assertEquals($en, getTranslations(\"xx\"), \"Locale not found\");\n    }\n\n    /**\n     * Test normalizeLocaleCode() function\n     */\n    public function testNormalizeLocaleCode(): void\n    {\n        // test script not normalized\n        $this->assertEquals(\"zh_Hans\", normalizeLocaleCode(\"zh_hans\"), \"Script not normalized\");\n        // test region not normalized\n        $this->assertEquals(\"pt_BR\", normalizeLocaleCode(\"pt_br\"), \"Region not normalized\");\n        // test script and region not normalized\n        $this->assertEquals(\"zh_Hans_CN\", normalizeLocaleCode(\"zh_hans_cn\"), \"Script and region not normalized\");\n        // test numeric region\n        $this->assertEquals(\"es_419\", normalizeLocaleCode(\"es_419\"), \"Numeric region not normalized\");\n        // test dashes instead of underscores\n        $this->assertEquals(\"zh_Hans\", normalizeLocaleCode(\"zh-hans\"), \"Dashes not normalized\");\n        // test uppercase\n        $this->assertEquals(\"zh_Hans\", normalizeLocaleCode(\"ZH-HANS\"), \"Uppercase not normalized\");\n        // test invalid locale format\n        $this->assertEquals(\"en\", normalizeLocaleCode(\"xxxx-XXX-XXXXX\"), \"Invalid locale format not normalized\");\n    }\n}\n"
  },
  {
    "path": "tests/phpunit/phpunit.xml",
    "content": "<?xml version=\"1.0\"?>\n<phpunit colors=\"true\">\n  <testsuites>\n    <testsuite name=\"main\">\n      <directory suffix=\"Test.php\">../</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>\n"
  }
]