├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── workflows │ ├── gh-pages.yml │ └── prettier.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── CNAME ├── index.html ├── script.js └── style.css └── runkit └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [DenverCoder1] 2 | patreon: 3 | open_collective: 4 | ko_fi: 5 | tidelift: 6 | community_bridge: 7 | liberapay: 8 | issuehunt: 9 | otechie: 10 | custom: 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: I have a question about Custom Icon Badges 4 | title: "" 5 | labels: "question" 6 | assignees: "" 7 | --- 8 | 9 | **Description** 10 | 11 | A brief description of the question or issue: 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | Fixes # 6 | 7 | ### Type of change 8 | 9 | 10 | 11 | - [ ] Bug fix (added a non-breaking change which fixes an issue) 12 | - [ ] New feature (added a non-breaking change which adds functionality) 13 | - [ ] Updated documentation (updated the readme, templates, or other repo files) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | 16 | ## Checklist: 17 | 18 | - [ ] I have checked to make sure no other [pull requests](https://github.com/DenverCoder1/dynamic-badge-formatter/pulls?q=is%3Apr+sort%3Aupdated-desc+) are open for this issue 19 | - [ ] The code is properly formatted and is consistent with the existing code style 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'demo/**' 9 | - '.github/workflows/gh-pages.yml' 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | deploy: 17 | concurrency: ci-${{ github.ref }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Deploy 24 | uses: JamesIves/github-pages-deploy-action@v4 25 | with: 26 | branch: gh-pages 27 | folder: demo 28 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Format with Prettier 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | prettier: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | # Make sure the actual branch is checked out when running on pull requests 18 | ref: ${{ github.head_ref }} 19 | 20 | - name: Prettify code 21 | uses: creyD/prettier_action@v4.2 22 | with: 23 | prettier_options: "--write **/*.{md,js} !**/*.min.js --print-width 120" 24 | commit_message: "style: Formatted code with Prettier" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting me via direct message on Twitter, Reddit, or Discord. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have a way to improve this project. 4 | 5 | If you are making a significant change, please open an issue before creating a pull request. This will allow us to discuss the design and implementation. 6 | 7 | Make sure your request is meaningful and it would help if you test the project before submitting a pull request. 8 | 9 | ### Testing with Runkit 10 | 11 | 1. Sign in to **Runkit** or create a new account at 12 | 2. Create a new notebook 13 | 3. Paste the contents of [`index.js`](./runkit/index.js) into the notebook 14 | 4. Click `endpoint` to get your own endpoint to run requests against 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonah Lawrence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Badge Formatter 2 | 3 | [![stars](https://custom-icon-badges.herokuapp.com/github/stars/DenverCoder1/dynamic-badge-formatter?logo=star)](https://github.com/DenverCoder1/dynamic-badge-formatter/stargazers "stars") [![issues](https://custom-icon-badges.herokuapp.com/github/issues-raw/DenverCoder1/dynamic-badge-formatter?logo=issue)](https://github.com/DenverCoder1/dynamic-badge-formatter/issues "issues") [![license](https://custom-icon-badges.herokuapp.com/github/license/DenverCoder1/dynamic-badge-formatter?logo=law&logoColor=white)](https://github.com/DenverCoder1/dynamic-badge-formatter/blob/main/LICENSE?rgh-link-date=2021-08-09T18%3A10%3A26Z "license MIT") [![discord](https://custom-icon-badges.herokuapp.com/discord/819650821314052106?color=7289DA&logo=comments&label=discord&logoColor=white)](https://discord.gg/fPrdqh3Zfu "Dev Pro Tips Discussion & Support Server") 4 | 5 | Format [Shields Dynamic Badges](https://shields.io/#dynamic-badge) to look consistent using formatters for metrics, versions, stars and more. 6 | 7 | Dynamic Badge Formatter works alongside [shields.io](https://shields.io/) using [Endpoint Badges](https://shields.io/endpoint) with a [Runkit Endpoint](https://runkit.com/denvercoder1/dynamic-badge-formatter). 8 | 9 | ## ⚡ How to use 10 | 11 | The easiest way to get started is to [try out the demo site](https://dynamic-badge-formatter.demolab.com/)! 12 | 13 | [![demo site](https://user-images.githubusercontent.com/20955511/174156046-a84dd1c7-d08a-4d5f-bc73-c79cca980180.png)](https://dynamic-badge-formatter.demolab.com/) 14 | 15 | ### Advanced steps 16 | 17 | 1. Choose a JSON, XML, or YAML data URL to extract data from. 18 | 19 | 2. Create a query using a [JSONPath](https://jsonpath.com/) (for JSON or YAML) or [XPath](http://xpather.com/) (for XML) expression. 20 | 21 | 3. Set the `url` and `query` parameters at the endpoint , using `/json` for JSON, `/xml` for XML, and `/yaml` for YAML. 22 | 23 | 4. Set additional customizations as query parameters, such as the `color`, `label`, `labelColor`, `logo`, etc., and specify a `formatter` to use (see below). 24 | 25 | 5. URL Encode the new endpoint URL and append it after `https://img.shields.io/endpoint?url=`. You can also do this by [pasting the URL](https://user-images.githubusercontent.com/20955511/173730516-1470689e-0e05-4761-89f4-4aa7d8fcb023.png) at [shields.io/endpoint](https://shields.io/endpoint). 26 | 27 | ### Example 28 | 29 | The following is a JSON API I want to use for displaying data. I want to display the stars but formatted as a metric (eg. `"3.2k"` instead of `"3227"`). To extract the star count from the JSON, I will use the query `$.stars`. 30 | 31 | ```jsonc 32 | // https://api.github-star-counter.workers.dev/user/DenverCoder1 33 | { 34 | "stars": 3227, 35 | "forks": 1207 36 | } 37 | ``` 38 | 39 | To create the Runkit URL, pass the `query`, `url`, and additional parameters to the endpoint. In this example, I set `formatter` to `metric`, `label` to `stars`, `color` to `green`, and `logo` to `github`. 40 | 41 | ``` 42 | https://dynamic-badge-formatter-ynrxn78r2oye.runkit.sh/json?query=$.stars&url=https://api.github-star-counter.workers.dev/user/DenverCoder1&formatter=metric&label=stars&color=green&logo=github 43 | ``` 44 | 45 | Using the customizer at , I can turn this endpoint into a badge. 46 | 47 | ``` 48 | https://img.shields.io/endpoint?url=https%3A%2F%2Fdynamic-badge-formatter-ynrxn78r2oye.runkit.sh%2Fjson%3Fquery%3D%24.stars%26url%3Dhttps%3A%2F%2Fapi.github-star-counter.workers.dev%2Fuser%2FDenverCoder1%26formatter%3Dmetric%26label%3Dstars%26color%3Dgreen%26logo%3Dgithub 49 | ``` 50 | 51 | Result: 52 | 53 | ![preview](https://img.shields.io/endpoint?url=https%3A%2F%2Fdynamic-badge-formatter-ynrxn78r2oye.runkit.sh%2Fjson%3Fquery%3D%24.stars%26url%3Dhttps%3A%2F%2Fapi.github-star-counter.workers.dev%2Fuser%2FDenverCoder1%26formatter%3Dmetric%26label%3Dstars%26color%3Dgreen%26logo%3Dgithub) 54 | 55 | ## ⚡ Formatters 56 | 57 | The following values are supported for the `formatter` parameter: 58 | 59 | | Formatter | Description | Example | 60 | | -------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | 61 | | `metric` | Formats a number as a short metric (eg. `3.4k`, `12.3M`) | ![before](https://img.shields.io/badge/before-3400-cc6060)
![after](https://img.shields.io/badge/after-3.4k-2ea44f) | 62 | | `starRating` | Formats a number as stars (eg. `★★★★½`) | ![before](https://img.shields.io/badge/before-4.5-cc6060)
![after](https://img.shields.io/badge/after-★★★★½-2ea44f) | 63 | | `ordinalNumber` | Formats a number with an ordinal suffix (eg. `9ᵗʰ`) | ![before](https://img.shields.io/badge/before-9-cc6060)
![after](https://img.shields.io/badge/after-9ᵗʰ-2ea44f) | 64 | | `omitv` | Removes a `v` as a prefix from a version number (eg. `v1.2.3` becomes `1.2.3`) | ![before](https://img.shields.io/badge/before-v1.2.3-cc6060)
![after](https://img.shields.io/badge/after-1.2.3-2ea44f) | 65 | | `addv` | Adds a `v` as a prefix from a version number (eg. `1.2.3` becomes `v1.2.3`) | ![before](https://img.shields.io/badge/before-1.2.3-cc6060)
![after](https://img.shields.io/badge/after-v1.2.3-2ea44f) | 66 | | `formatDate` | Formats dates as a month and year, "today" or "yesterday" can appear for recent dates | ![before](https://img.shields.io/badge/before-2019--01--01-cc6060)
![after](https://img.shields.io/badge/after-january%202019-2ea44f) | 67 | | `formatRelativeDate` | Formats a UNIX Timestamp in seconds as a relative time (eg. `3 days ago`) | ![before](https://img.shields.io/badge/before-1655162563-cc6060)
![after](https://img.shields.io/badge/after-3%20days%20ago-2ea44f) | 68 | 69 | ## ⚙️ Other Parameters 70 | 71 | | Parameter | Type | Description | 72 | | -------------- | --------- | -------------------------------------------------------------------------------------------------- | 73 | | `url` | `string` | `required` The JSON, XML, or YAML data URL to fetch a message value from | 74 | | `query` | `string` | `required` The JSONPath or XPath query for extracting a field for the value | 75 | | `label` | `string` | `optional` The label to use for the badge, default: "custom badge" | 76 | | `color` | `string` | `optional` The color to use for the badge, default: "blue" | 77 | | `labelColor` | `string` | `optional` The color to use for the label, default: "grey" | 78 | | `isError` | `boolean` | `optional` If true, the badge color is overriden to be red, default: false | 79 | | `logo` | `string` | `optional` A named logo to use from Simple Icons or base64 encoded SVG, default: none | 80 | | `namedLogo` | `string` | `optional` The name of a logo to use from Simple Icons, overrides `logo`, default: none | 81 | | `logoSvg` | `string` | `optional` The base64 encoded SVG content of a logo to use, overrides `logo`, default: none | 82 | | `logoColor` | `string` | `optional` The color to use for the logo, default: none | 83 | | `logoWidth` | `number` | `optional` The width of the logo, default: none | 84 | | `logoPosition` | `number` | `optional` The position offset of the logo, default: none | 85 | | `style` | `string` | `optional` The badge style (plastic, flat, flat-square, for-the-badge, or social), default: "flat" | 86 | | `cacheSeconds` | `number` | `optional` The number of seconds to cache the response, default: 300 | 87 | | `prefix` | `string` | `optional` A prefix to use before the message, default: none | 88 | | `suffix` | `string` | `optional` A suffix to use after the message, default: none | 89 | | `formatter` | `string` | `optional` The name of a formatter to use on the message (see options above), default: none | 90 | 91 | ## 🤗 Contributing 92 | 93 | We welcome contributions! 94 | 95 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. 96 | 97 | ## 📤 Deploying it on your own 98 | 99 |
100 | Deploy to Runkit 101 | 102 | 1. Sign in to **Runkit** or create a new account at 103 | 2. Create a new notebook 104 | 3. Paste the contents of [`index.js`](./runkit/index.js) into the notebook 105 | 4. Click `endpoint` to get your endpoint to run requests against 106 | 107 |
108 | 109 | ## 💬 Questions? 110 | 111 | Feel free to [open an issue](http://github.com/DenverCoder1/dynamic-badge-formatter/issues/new). 112 | 113 | ## ❤️ Thanks 114 | 115 | - [Shields.io](https://github.com/badges/shields) for all the great work they have done with creating tools for creating Dynamic and Endpoint Badges 116 | 117 | ## 📚 License 118 | 119 | This project is licensed under the [MIT license](LICENSE.md). 120 | 121 | Some formatters make use of code written for [shields.io](https://shields.io/) in the [public domain](https://github.com/badges/shields/blob/master/LICENSE). 122 | 123 | ## 🤩 Support 124 | 125 | 💙 If you like this project, give it a ⭐ and share it with friends! 126 | 127 |

128 | Youtube 129 | Sponsor with Github 130 |

131 | 132 | [☕ Buy me a coffee](https://ko-fi.com/jlawrence) 133 | -------------------------------------------------------------------------------- /demo/CNAME: -------------------------------------------------------------------------------- 1 | dynamic-badge-formatter.demolab.com -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Dynamic Badge Formatter 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

Dynamic Badge Formatter

27 |
28 | 29 | 30 |
31 | 32 | Sponsor 35 | 36 | View on GitHub 39 | 40 | Star 44 |
45 | 46 |
47 |
48 |

Properties

49 | 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 |
75 | 76 |
77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 103 | 104 |

Customization Options

105 |
106 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
126 | 127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 |
138 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
148 | 149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
158 | 159 |
160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
168 | 169 |
170 | 171 | 173 | 174 | 175 | 176 | 177 | 184 |
185 | 186 |
187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
195 | 196 |
197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 |
205 | 206 |
207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 |
215 | 216 |
217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 |
225 |
226 |
227 | 228 |
229 |

Preview

230 | 231 |
232 | 233 | 234 | Preview 236 | 237 |
238 |

Image Source

239 |
https://img.shields.io/endpoint?url=https%3A%2F%2Fdynamic-badge-formatter-ynrxn78r2oye.runkit.sh%2F%2F<DATA_TYPE>%3Furl%3D<URL>%26query%3D<QUERY>%26formatter%3D<FORMATTER>%26style%3D<STYLE>
240 | 243 |
244 |
245 |

Markdown

246 |
![badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdynamic-badge-formatter-ynrxn78r2oye.runkit.sh%2F%2F<DATA_TYPE>%3Furl%3D<URL>%26query%3D<QUERY>%26formatter%3D<FORMATTER>%26style%3D<STYLE>)
247 | 250 |
251 |
252 |
253 |
254 | 255 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /demo/script.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const inputs = Array.from(document.querySelectorAll(".input-group select, .input-group input")); 3 | const loader = document.querySelector("#preview .loader"); 4 | const previewImage = document.querySelector("#preview img"); 5 | const previewLink = document.querySelector("#preview a"); 6 | const markdownBlock = document.querySelector("#preview .markdown pre"); 7 | const imageSourceBlock = document.querySelector("#preview .image-source pre"); 8 | const copyButtons = Array.from(document.querySelectorAll(".output .copy-button")); 9 | 10 | function getBadgeURL(dataType, params) { 11 | const baseRunkit = "https://dynamic-badge-formatter-ynrxn78r2oye.runkit.sh"; 12 | const baseShieldsEndpoint = "https://img.shields.io/endpoint"; 13 | const runkitURL = `${baseRunkit}/${dataType}?${Object.keys(params) 14 | .map((key) => `${key}=${encodeURIComponent(params[key])}`) 15 | .join("&")}`; 16 | return `${baseShieldsEndpoint}?url=${encodeURIComponent(runkitURL)}`; 17 | } 18 | 19 | function toggleLoader(show) { 20 | loader.style.display = show ? "block" : "none"; 21 | previewImage.style.display = show ? "none" : "block"; 22 | } 23 | 24 | function updatePreview() { 25 | const params = inputs.reduce((acc, el) => { 26 | const obj = { ...acc }; 27 | if (el.value !== "") { 28 | obj[el.id] = el.value; 29 | } 30 | return obj; 31 | }, {}); 32 | 33 | if (!params.dataType || !params.query || !params.url) { 34 | previewImage.src = 35 | "https://img.shields.io/badge/preview-fill%20in%20a%20data%20url%20and%20query%20to%20see%20a%20preview-blue.svg"; 36 | return; 37 | } 38 | 39 | const { dataType, href, ...rest } = params; 40 | const shieldsEndpoint = getBadgeURL(dataType, rest); 41 | 42 | toggleLoader(true); 43 | 44 | previewLink.removeAttribute("href"); 45 | previewImage.src = shieldsEndpoint; 46 | imageSourceBlock.innerText = shieldsEndpoint; 47 | let markdown = `![badge](${shieldsEndpoint})`; 48 | if (href) { 49 | previewLink.href = href; 50 | markdown = `[${markdown}](${href})`; 51 | } 52 | markdownBlock.innerText = markdown; 53 | copyButtons.forEach((el) => { 54 | el.disabled = false; 55 | }); 56 | } 57 | 58 | inputs.forEach((el) => { 59 | el.addEventListener("change", updatePreview); 60 | }); 61 | 62 | previewImage.addEventListener("load", () => { 63 | toggleLoader(false); 64 | }); 65 | 66 | copyButtons.forEach((el) => { 67 | el.addEventListener("click", (event) => { 68 | const text = event.target.closest(".text-output").querySelector("pre").innerText; 69 | // copy using the clipboard API 70 | if (navigator.clipboard) { 71 | navigator.clipboard.writeText(text).then(() => { 72 | event.target.innerText = "Copied!"; 73 | setTimeout(() => { 74 | event.target.innerText = "Copy to Clipboard"; 75 | }, 1000); 76 | }); 77 | } 78 | // fallback to copying the text using execCommand 79 | else { 80 | const textarea = document.createElement("textarea"); 81 | textarea.value = text; 82 | document.body.appendChild(textarea); 83 | textarea.select(); 84 | document.execCommand("copy"); 85 | document.body.removeChild(textarea); 86 | event.target.innerText = "Copied!"; 87 | setTimeout(() => { 88 | event.target.innerText = "Copy to clipboard"; 89 | }, 1000); 90 | } 91 | }); 92 | }); 93 | })(); 94 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | *, 8 | *::before, 9 | *::after { 10 | -webkit-box-sizing: inherit; 11 | -moz-box-sizing: inherit; 12 | box-sizing: inherit; 13 | } 14 | 15 | :root { 16 | --background: #090d13; 17 | --card-background: #0d1117; 18 | --text: #efefef; 19 | --card-element-background: #2a2e34; 20 | --stroke: #737373; 21 | --blue-light: #1976d2; 22 | --blue-transparent: #2196f320; 23 | --blue-dark: #1565c0; 24 | --button-outline: black; 25 | --red: #ff6464; 26 | --yellow: #a59809; 27 | --yellow-light: #716800; 28 | --link: #b7f9ff; 29 | --link-visited: #f2a4ff; 30 | } 31 | 32 | body { 33 | background: var(--background); 34 | color: var(--text); 35 | font-family: Segoe UI, Ubuntu, sans-serif; 36 | } 37 | 38 | a { 39 | color: var(--link); 40 | } 41 | 42 | a:visited { 43 | color: var(--link-visited); 44 | } 45 | 46 | footer { 47 | margin-top: 3em; 48 | } 49 | 50 | .header-link { 51 | text-decoration: none; 52 | } 53 | 54 | .center { 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | } 59 | 60 | .github { 61 | gap: 0.5em; 62 | } 63 | 64 | .loader { 65 | margin-left: 1.5em; 66 | } 67 | 68 | .input-group select, 69 | .input-group input { 70 | padding: 0.4em; 71 | margin: 0.25em; 72 | border-radius: 0.25em; 73 | background: var(--card-element-background); 74 | color: var(--text); 75 | border: 1px solid var(--stroke); 76 | } 77 | 78 | @media (max-width: 680px) { 79 | .input-group select, 80 | .input-group input { 81 | display: block; 82 | width: 100%; 83 | margin: 0.75em 0; 84 | } 85 | } 86 | 87 | .url-input { 88 | width: 380px; 89 | max-width: 100%; 90 | } 91 | 92 | div.text-output pre { 93 | white-space: pre-wrap; 94 | white-space: -moz-pre-wrap; 95 | white-space: -pre-wrap; 96 | white-space: -o-pre-wrap; 97 | word-wrap: break-word; 98 | background: var(--card-element-background); 99 | padding: 1em; 100 | border-radius: 0.5em; 101 | max-width: 650px; 102 | } 103 | 104 | span.tooltip { 105 | margin: 0 0.15em; 106 | vertical-align: middle; 107 | } 108 | 109 | .container { 110 | margin: auto; 111 | max-width: 100%; 112 | display: flex; 113 | flex-wrap: wrap; 114 | justify-content: center; 115 | } 116 | 117 | .properties, 118 | .output { 119 | margin: 10px; 120 | background: var(--card-background); 121 | padding: 25px; 122 | padding-top: 0; 123 | border: 1px solid var(--card-element-background); 124 | border-radius: 6px; 125 | } 126 | 127 | .properties { 128 | max-width: 628px; 129 | } 130 | 131 | .output { 132 | max-width: 420px; 133 | } 134 | 135 | @media only screen and (max-width: 1115px) { 136 | .properties, 137 | .output { 138 | max-width: 100%; 139 | width: 100%; 140 | } 141 | } 142 | 143 | :not(.btn):focus { 144 | outline: var(--blue-light) auto 2px; 145 | } 146 | 147 | .btn { 148 | max-width: 100%; 149 | background-color: var(--blue-light); 150 | color: white; 151 | padding: 10px 20px; 152 | border: none; 153 | border-radius: 6px; 154 | cursor: pointer; 155 | font-family: inherit; 156 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 157 | transition: 0.2s ease-in-out; 158 | } 159 | 160 | .btn:focus { 161 | outline: var(--button-outline) auto 2px; 162 | } 163 | 164 | .btn:hover { 165 | background-color: var(--blue-dark); 166 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); 167 | } 168 | 169 | .btn:disabled { 170 | cursor: not-allowed; 171 | background: var(--blue-transparent); 172 | box-shadow: none; 173 | } 174 | 175 | .btn.copy-button { 176 | width: 152px; 177 | } 178 | 179 | /* tooltips */ 180 | 181 | /* bubble */ 182 | .tooltip:before { 183 | content: attr(title); 184 | height: auto; 185 | width: auto; 186 | position: absolute; 187 | background: #4a4a4afa; 188 | border-radius: 4px; 189 | color: white; 190 | line-height: 30px; 191 | font-size: 0.9em; 192 | padding: 0 12px; 193 | transform: translate(-55px, -35px); 194 | transition: all 0.2s ease-out; 195 | pointer-events: none; 196 | opacity: 0; 197 | } 198 | 199 | /* triangle */ 200 | .tooltip:after { 201 | content: ""; 202 | position: absolute; 203 | border-style: solid; 204 | border-color: #4a4a4afa transparent transparent transparent; 205 | transform: translate(-10px, -5px); 206 | transition: all 0.2s ease-out; 207 | pointer-events: none; 208 | opacity: 0; 209 | } 210 | 211 | .tooltip:hover:before, 212 | .tooltip:hover:after { 213 | opacity: 1; 214 | } 215 | 216 | /* 217 | * three-dots - v0.2.3 218 | * CSS loading animations made with single element 219 | * https://nzbin.github.io/three-dots/ 220 | * 221 | * Copyright (c) 2018 nzbin 222 | * Released under MIT License 223 | */ 224 | .dot-elastic { 225 | position: relative; 226 | width: 10px; 227 | height: 10px; 228 | border-radius: 5px; 229 | background-color: #b7f9ff; 230 | color: #b7f9ff; 231 | -webkit-animation: dot-elastic 1s infinite linear; 232 | animation: dot-elastic 1s infinite linear; 233 | } 234 | .dot-elastic::before, 235 | .dot-elastic::after { 236 | content: ""; 237 | display: inline-block; 238 | position: absolute; 239 | top: 0; 240 | } 241 | .dot-elastic::before { 242 | left: -15px; 243 | width: 10px; 244 | height: 10px; 245 | border-radius: 5px; 246 | background-color: #b7f9ff; 247 | color: #b7f9ff; 248 | -webkit-animation: dot-elastic-before 1s infinite linear; 249 | animation: dot-elastic-before 1s infinite linear; 250 | } 251 | .dot-elastic::after { 252 | left: 15px; 253 | width: 10px; 254 | height: 10px; 255 | border-radius: 5px; 256 | background-color: #b7f9ff; 257 | color: #b7f9ff; 258 | -webkit-animation: dot-elastic-after 1s infinite linear; 259 | animation: dot-elastic-after 1s infinite linear; 260 | } 261 | 262 | @-webkit-keyframes dot-elastic-before { 263 | 0% { 264 | transform: scale(1, 1); 265 | } 266 | 25% { 267 | transform: scale(1, 1.5); 268 | } 269 | 50% { 270 | transform: scale(1, 0.67); 271 | } 272 | 75% { 273 | transform: scale(1, 1); 274 | } 275 | 100% { 276 | transform: scale(1, 1); 277 | } 278 | } 279 | 280 | @keyframes dot-elastic-before { 281 | 0% { 282 | transform: scale(1, 1); 283 | } 284 | 25% { 285 | transform: scale(1, 1.5); 286 | } 287 | 50% { 288 | transform: scale(1, 0.67); 289 | } 290 | 75% { 291 | transform: scale(1, 1); 292 | } 293 | 100% { 294 | transform: scale(1, 1); 295 | } 296 | } 297 | @-webkit-keyframes dot-elastic { 298 | 0% { 299 | transform: scale(1, 1); 300 | } 301 | 25% { 302 | transform: scale(1, 1); 303 | } 304 | 50% { 305 | transform: scale(1, 1.5); 306 | } 307 | 75% { 308 | transform: scale(1, 1); 309 | } 310 | 100% { 311 | transform: scale(1, 1); 312 | } 313 | } 314 | @keyframes dot-elastic { 315 | 0% { 316 | transform: scale(1, 1); 317 | } 318 | 25% { 319 | transform: scale(1, 1); 320 | } 321 | 50% { 322 | transform: scale(1, 1.5); 323 | } 324 | 75% { 325 | transform: scale(1, 1); 326 | } 327 | 100% { 328 | transform: scale(1, 1); 329 | } 330 | } 331 | @-webkit-keyframes dot-elastic-after { 332 | 0% { 333 | transform: scale(1, 1); 334 | } 335 | 25% { 336 | transform: scale(1, 1); 337 | } 338 | 50% { 339 | transform: scale(1, 0.67); 340 | } 341 | 75% { 342 | transform: scale(1, 1.5); 343 | } 344 | 100% { 345 | transform: scale(1, 1); 346 | } 347 | } 348 | @keyframes dot-elastic-after { 349 | 0% { 350 | transform: scale(1, 1); 351 | } 352 | 25% { 353 | transform: scale(1, 1); 354 | } 355 | 50% { 356 | transform: scale(1, 0.67); 357 | } 358 | 75% { 359 | transform: scale(1, 1.5); 360 | } 361 | 100% { 362 | transform: scale(1, 1); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /runkit/index.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const express = require("@runkit/runkit/express-endpoint/1.0.0"); 3 | const moment = require("moment"); 4 | 5 | const app = express(exports); 6 | 7 | /** 8 | * This object contains text formatters adapted from shields.io, a project in the public domain. 9 | * @see https://github.com/badges/shields/blob/master/services/text-formatters.js 10 | */ 11 | const formatters = { 12 | starRating(rating, max = 5) { 13 | const flooredRating = Math.floor(rating); 14 | let stars = ""; 15 | while (stars.length < flooredRating) { 16 | stars += "★"; 17 | } 18 | const decimal = rating - flooredRating; 19 | if (decimal >= 0.875) { 20 | stars += "★"; 21 | } else if (decimal >= 0.625) { 22 | stars += "¾"; 23 | } else if (decimal >= 0.375) { 24 | stars += "½"; 25 | } else if (decimal >= 0.125) { 26 | stars += "¼"; 27 | } 28 | 29 | while (stars.length < max) { 30 | stars += "☆"; 31 | } 32 | return stars; 33 | }, 34 | 35 | ordinalNumber(n) { 36 | const s = ["ᵗʰ", "ˢᵗ", "ⁿᵈ", "ʳᵈ"]; 37 | const v = n % 100; 38 | return n + (s[(v - 20) % 10] || s[v] || s[0]); 39 | }, 40 | 41 | // Given a number (positive or negative), string with appropriate unit in the metric system, SI. 42 | // Note: numbers beyond the peta- cannot be represented as integers in JS. 43 | metric(n) { 44 | const metricPrefix = ["k", "M", "G", "T", "P", "E", "Z", "Y"]; 45 | const metricPower = metricPrefix.map((a, i) => Math.pow(1000, i + 1)); 46 | for (let i = metricPrefix.length - 1; i >= 0; i--) { 47 | const limit = metricPower[i]; 48 | const absN = Math.abs(n); 49 | if (absN >= limit) { 50 | const scaledN = absN / limit; 51 | if (scaledN < 10) { 52 | // For "small" numbers, display one decimal digit unless it is 0. 53 | const oneDecimalN = scaledN.toFixed(1); 54 | if (oneDecimalN.charAt(oneDecimalN.length - 1) !== "0") { 55 | const res = `${oneDecimalN}${metricPrefix[i]}`; 56 | return n > 0 ? res : `-${res}`; 57 | } 58 | } 59 | const roundedN = Math.round(scaledN); 60 | if (roundedN < 1000) { 61 | const res = `${roundedN}${metricPrefix[i]}`; 62 | return n > 0 ? res : `-${res}`; 63 | } else { 64 | const res = `1${metricPrefix[i + 1]}`; 65 | return n > 0 ? res : `-${res}`; 66 | } 67 | } 68 | } 69 | return `${n}`; 70 | }, 71 | 72 | // Remove the starting v in a string. 73 | omitv(version) { 74 | if (version.charCodeAt(0) === 118) { 75 | return version.slice(1); 76 | } 77 | return version; 78 | }, 79 | 80 | // Add a starting v to the version unless: 81 | // - it does not start with a digit 82 | // - it is a date (yyyy-mm-dd) 83 | addv(version) { 84 | const ignoredVersionPatterns = /^[^0-9]|[0-9]{4}-[0-9]{2}-[0-9]{2}/; 85 | version = `${version}`; 86 | if (version.startsWith("v") || ignoredVersionPatterns.test(version)) { 87 | return version; 88 | } else { 89 | return `v${version}`; 90 | } 91 | }, 92 | 93 | maybePluralize(singular, countable, plural) { 94 | plural = plural || `${singular}s`; 95 | 96 | if (countable && countable.length === 1) { 97 | return singular; 98 | } else { 99 | return plural; 100 | } 101 | }, 102 | 103 | formatDate(d) { 104 | const date = moment(d); 105 | const dateString = date.calendar(null, { 106 | lastDay: "[yesterday]", 107 | sameDay: "[today]", 108 | lastWeek: "[last] dddd", 109 | sameElse: "MMMM YYYY", 110 | }); 111 | // Trim current year from date string 112 | return dateString.replace(` ${moment().year()}`, "").toLowerCase(); 113 | }, 114 | 115 | formatRelativeDate(timestamp) { 116 | return moment() 117 | .to(moment.unix(parseInt(timestamp, 10))) 118 | .toLowerCase(); 119 | }, 120 | }; 121 | 122 | /** 123 | * Schema for shields.io endpoint badge content 124 | */ 125 | const contentSchema = [ 126 | "schemaVersion", // number, required, must be 1 127 | "label", // string, required 128 | "message", // string, required 129 | "color", // string, default: "blue" 130 | "labelColor", // string, default: "grey" 131 | "isError", // boolean, default: false 132 | "namedLogo", // string 133 | "logoSvg", // string 134 | "logoColor", // string 135 | "logoWidth", // number 136 | "logoPosition", // number 137 | "style", // string, default: "flat" 138 | "cacheSeconds", // number, default: 300 139 | ]; 140 | 141 | /** 142 | * Default route for creating the content for the shields.io endpoint 143 | * 144 | * Path parameters: 145 | * - dataType: string, optional, one of "json", "xml", or "yaml", default: "json" 146 | * 147 | * Query parameters: 148 | * - url: string, required, the JSON, XML, or YAML data URL to fetch a message value from 149 | * - query: string, required, the JSONPath or XPath query for extracting a field for the value 150 | * - label: string, optional, the label to use for the badge, default: "custom badge" 151 | * - color: string, optional, the color to use for the badge, default: "blue" 152 | * - labelColor: string, optional, the color to use for the label, default: "grey" 153 | * - isError: boolean, optional, if true, the badge color is overriden to be red, default: false 154 | * - logo: string, optional, a named logo or a base64 encoded SVG content of a logo to use default: none 155 | * - namedLogo: string, optional, the name of a logo to use from Simple Icons, overrides logo, default: none 156 | * - logoSvg: string, optional, the base64 encoded SVG content of a logo to use, overrides logo, default: none 157 | * - logoColor: string, optional, the color to use for the logo, default: none 158 | * - logoWidth: number, optional, the width of the logo, default: none 159 | * - logoPosition: number, optional, the position offset of the logo, default: none 160 | * - style: string, optional, the style of the badge (plastic, flat, flat-square, for-the-badge, or social), default: "flat" 161 | * - cacheSeconds: number, optional, the number of seconds to cache the response, default: 300 162 | * - prefix: string, optional, a prefix to use before the message, default: none 163 | * - suffix: string, optional, a suffix to use after the message, default: none 164 | * - formatter: string, optional, the name of a formatter to use on the message, default: none 165 | */ 166 | app.get("/:dataType?", async (req, res) => { 167 | try { 168 | const dataType = req.params.dataType || "json"; 169 | if (!["json", "xml", "yaml"].includes(dataType)) { 170 | throw new Error(`dataType must be json, xml, or yaml`); 171 | } 172 | 173 | const { query, url } = req.query; 174 | 175 | const baseUrl = `https://img.shields.io/badge/dynamic/${dataType}.json`; 176 | 177 | const result = await axios.get(`${baseUrl}?${new URLSearchParams({ query, url })}`); 178 | 179 | // get returned data and override with any query string parameters 180 | const merged = { ...result.data, ...req.query }; 181 | 182 | // if logo is set, use it as a default for namedLogo/logoSvg depending on its content 183 | if (/^data:/.test(merged.logo)) { 184 | merged.logoSvg = merged.logoSvg || merged.logo; 185 | } else if (merged.logo) { 186 | merged.namedLogo = merged.namedLogo || merged.logo; 187 | } 188 | 189 | // format the badge message 190 | if (merged.formatter in formatters) { 191 | merged.message = formatters[merged.formatter](merged.message); 192 | } 193 | 194 | // add the prefix and suffix if they are set 195 | const prefix = req.query.prefix || ""; 196 | const suffix = req.query.suffix || ""; 197 | merged.message = `${prefix}${merged.message}${suffix}`; 198 | 199 | const content = { schemaVersion: 1 }; 200 | contentSchema.forEach((key) => { 201 | if (typeof merged[key] === "string") { 202 | content[key] = merged[key]; 203 | } 204 | }); 205 | res.json(content); 206 | } catch (e) { 207 | res.json({ 208 | schemaVersion: 1, 209 | label: "error", 210 | message: e.message, 211 | isError: true, 212 | }); 213 | } 214 | }); 215 | --------------------------------------------------------------------------------