diff --git a/.gitignore b/.gitignore index 955831b..0b5b141 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ /analytics.json /coverage.html /redis -/secret.json /ServerScript +/secret.json +/.github-user-tokens.json # Installed npm modules node_modules diff --git a/.travis.yml b/.travis.yml index 3a844f8..c2a9220 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ -before_install: sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ +before_install: + - sudo apt-get update -myqq + - sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ language: node_js node_js: - - 0.10 + - 6 branches: only: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a72fd1..0ecd0b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,19 @@ shields.io. Note that the root gets redirected to . For testing purposes, you can go to `http://localhost/try.html`. You should modify that file. The "real" root, `http://localhost/index.html`, -gets generated from the `try.html` file. +gets generated from the `try.html` file with a `make website`. + +## Ground rules + +- The left-hand side of a badge should not advertize. It should be a noun + describing succinctly the meaning of the right-hand-side data. +- New query parameters (such as `?label=` or `?style=`) should apply to any + requested badge. They must be registered in the cache (see `LruCache` in + `server.js`). +- The format of new badges should be of the form + `/VENDOR/SUBVENDOR-BADGE-SPECIFIC/PARAMETERS.format`. For instance, + `https://img.shields.io/gitter/room/nwjs/nw.js.svg`. The vendor is gitter, the + badge is for rooms, the parameter is nwjs/nw.js, and the format is svg. ## Implementations diff --git a/Dockerfile b/Dockerfile index 3c5a563..4f05c1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ -FROM node:0.12.7-onbuild +FROM node:0.12-onbuild ENV INFOSITE http://shields.io EXPOSE 80 diff --git a/INSTALL.md b/INSTALL.md index 20ba710..12b6ae9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,9 +6,9 @@ [![build status](http://img.shields.io/travis/badges/gh-badges.svg)](https://travis-ci.org/badges/gh-badges) -Make your own badges [here][badges]! +Make your own badges [here][badges]! (Quick guide: `https://img.shields.io/badge/left-right-f39f37.svg`.) -[badges]: +[badges]: # Install the API @@ -71,10 +71,13 @@ The format is the following: /* Textual information shown, in order. */ "text": [ "build", "passed" ], "format": "svg", // Also supports "json". - "colorscheme": "green" + "colorscheme": "green", /* … Or… */ "colorA": "#555", - "colorB": "#4c1" + "colorB": "#4c1", + /* See template/ for a list of available templates. + Each offers a different visual design. */ + "template": "flat" } ``` @@ -132,7 +135,7 @@ heroku open You can build and run the server locally using Docker. First build an image: ```console -$ build -t shields ./ +$ docker build -t shields ./ Sending build context to Docker daemon 3.923 MB Step 0 : FROM node:0.12.7-onbuild … @@ -153,6 +156,15 @@ http://[::1]:80/try.html Assuming Docker is running locally, you should be able to get to the application at http://localhost:8080/try.html. If you run Docker in a virtual machine (such as boot2docker or Docker Machine) then you will need to replace `localhost` with the actual IP address of that virtual machine. +# Main Server Sysadmin + +- DNS round-robin between https://vps197850.ovh.net/try.html and https://vps244529.ovh.net/try.html. +- Self-signed TLS certificates, but `img.shields.io` is behind CloudFlare, which provides signed certificates. +- Using node v0.12.7 because later versions, combined with node-canvas, give inaccurate badge measurements. +- Using forever (the node monitor) to automatically restart the server when it crashes. + +See https://github.com/badges/ServerScript for helper admin scripts. + # Links See for a story of the diff --git a/Makefile b/Makefile index 24bbfe8..066c345 100644 --- a/Makefile +++ b/Makefile @@ -4,17 +4,31 @@ favicon: node gh-badge.js '' '' '#bada55' .png > favicon.png website: - cat try.html | sed "s,\(,&https://img.shields.io," \ - | sed "s,var origin = '';,var origin = 'https://img.shields.io';," \ - | sed "s,
@@ -65,23 +69,23 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable

Build

- + - + - + - + - + @@ -89,638 +93,829 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable - + - + - + - + + + + + + + + + - + - + - + + + + + - + - + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
Travis: https://img.shields.io/travis/USER/REPO.svg
Travis branch: https://img.shields.io/travis/USER/REPO/BRANCH.svg
Wercker: https://img.shields.io/wercker/ci/wercker/docs.svg
TeamCity CodeBetter: https://img.shields.io/teamcity/codebetter/bt428.svg
TeamCity (simple build status): https://img.shields.io/teamcity/http/teamcity.jetbrains.com/s/bt345.svg
(full build status): https://img.shields.io/teamcity/http/teamcity.jetbrains.com/e/bt345.svg
AppVeyor: https://img.shields.io/appveyor/ci/gruntjs/grunt.svg
AppVeyor branch: https://img.shields.io/appveyor/ci/gruntjs/grunt/master.svg
Codeship: https://img.shields.io/codeship/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.svg
Codeship: https://img.shields.io/codeship/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/master.svg
Magnum CI: https://img.shields.io/magnumci/ci/96ffb83fa700f069024921b0702e76ff.svg
Magnum CI: https://img.shields.io/magnumci/ci/96ffb83fa700f069024921b0702e76ff/new-meta.svg
CircleCI: https://img.shields.io/circleci/project/BrightFlair/PHP.Gt.svg
CircleCI branch: https://img.shields.io/circleci/project/BrightFlair/PHP.Gt/master.svg
CircleCI token: https://img.shields.io/circleci/token/YOURTOKEN/project/BrightFlair/PHP.Gt/master.svg
Visual Studio Team services: https://img.shields.io/vso/build/larsbrinkhoff/953a34b9-5966-4923-a48a-c41874cfb5f5/1.svg
Shippable: https://img.shields.io/shippable/54d119db5ab6cc13528ab183.svg
Shippable branch: https://img.shields.io/shippable/54d119db5ab6cc13528ab183/master.svg
Snap CI branch: https://img.shields.io/snap-ci/ThoughtWorksStudios/eb_deployer/master.svg
Jenkins: https://img.shields.io/jenkins/s/https/jenkins.qa.ubuntu.com/precise-desktop-amd64_default.svg
Jenkins tests: https://img.shields.io/jenkins/t/https/jenkins.qa.ubuntu.com/precise-desktop-amd64_default.svg
Jenkins coverage: https://img.shields.io/jenkins/c/https/jenkins.qa.ubuntu.com/address-book-service-utopic-i386-ci.svg
Coveralls: https://img.shields.io/coveralls/jekyll/jekyll.svg
Coveralls branch: https://img.shields.io/coveralls/jekyll/jekyll/master.svg
SonarQube Coverage: https://img.shields.io/sonar/http/sonar.qatools.ru/ru.yandex.qatools.allure:allure-core/coverage.svg
SonarQube Tech Debt: https://img.shields.io/sonar/http/sonar.qatools.ru/ru.yandex.qatools.allure:allure-core/tech_debt.svg
TeamCity CodeBetter Coverage: https://img.shields.io/teamcity/coverage/bt1242.svg
Scrutinizer: https://img.shields.io/scrutinizer/g/filp/whoops.svg
Scrutinizer Coverage: https://img.shields.io/scrutinizer/coverage/g/filp/whoops.svg
Scrutinizer branch: https://img.shields.io/scrutinizer/coverage/g/phpmyadmin/phpmyadmin/master.svg
Scrutinizer Build: https://img.shields.io/scrutinizer/build/g/filp/whoops.svg
Codecov: https://img.shields.io/codecov/c/github/codecov/example-python.svg
Codecov branch: https://img.shields.io/codecov/c/github/codecov/example-python/master.svg
Codecov private: https://img.shields.io/codecov/c/token/YOURTOKEN/github/codecov/example-python.svg
Coverity Scan: https://img.shields.io/coverity/scan/3997.svg
Coverity Code Advisor On Demand Stream Badge: https://img.shields.io/coverity/ondemand/streams/STREAM.svg
Coverity Code Advisor On Demand Job Badge: https://img.shields.io/coverity/ondemand/jobs/JOB.svg
HHVM: https://img.shields.io/hhvm/symfony/symfony.svg
HHVM (branch): https://img.shields.io/hhvm/symfony/symfony/master.svg
SensioLabs Insight: https://img.shields.io/sensiolabs/i/45afb680-d4e6-4e66-93ea-bcfa79eb8a87.svg

Downloads

- - + + - - + + - - + + - - + + - + - + - + - + - + - + - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Github All Releases:
Github All Releases: https://img.shields.io/github/downloads/atom/atom/total.svg
Github Releases:
Github Releases: https://img.shields.io/github/downloads/atom/atom/latest/total.svg
Github Releases (by Release):
Github Releases (by Release): https://img.shields.io/github/downloads/atom/atom/v0.190.0/total.svg
Github Releases (by Asset):
Github Releases (by Asset): https://img.shields.io/github/downloads/atom/atom/latest/atom-amd64.deb.svg
npm: https://img.shields.io/npm/dm/localeval.svg
npm: https://img.shields.io/npm/dt/express.svg
Gem: https://img.shields.io/gem/dv/rails/stable.svg
Gem: https://img.shields.io/gem/dv/rails/4.1.0.svg
Gem: https://img.shields.io/gem/dtv/rails.svg
Gem: https://img.shields.io/gem/dt/rails.svg
NuGet: https://img.shields.io/nuget/dt/Microsoft.AspNet.Mvc.svg
MyGet: https://img.shields.io/myget/yolodev/dt/FSharpSupport.svg
Chocolatey: https://img.shields.io/chocolatey/dt/scriptcs.svg
PyPI: https://img.shields.io/pypi/dm/Django.svg
PyPI: https://img.shields.io/pypi/dw/Django.svg
PyPI: https://img.shields.io/pypi/dd/Django.svg
Crates.io: https://img.shields.io/crates/d/rustc-serialize.svg
Crates.io: https://img.shields.io/crates/dv/rustc-serialize.svg
Packagist: https://img.shields.io/packagist/dm/doctrine/orm.svg
Packagist: https://img.shields.io/packagist/dd/doctrine/orm.svg
Packagist: https://img.shields.io/packagist/dt/doctrine/orm.svg
Hex.pm: https://img.shields.io/hexpm/dw/plug.svg
Hex.pm: https://img.shields.io/hexpm/dd/plug.svg
Hex.pm: https://img.shields.io/hexpm/dt/plug.svg
WordPress: https://img.shields.io/wordpress/plugin/dt/akismet.svg
SourceForge: https://img.shields.io/sourceforge/dm/sevenzip.svg
SourceForge: https://img.shields.io/sourceforge/dw/sevenzip.svg
SourceForge: https://img.shields.io/sourceforge/dd/sevenzip.svg
SourceForge: https://img.shields.io/sourceforge/dt/sevenzip.svg
SourceForge: https://img.shields.io/sourceforge/dt/arianne/stendhal.svg
apm: https://img.shields.io/apm/dm/vim-mode.svg
Puppet Forge: https://img.shields.io/puppetforge/dt/camptocamp/openldap.svg
DUB: https://img.shields.io/dub/dd/vibe-d.svg
DUB: https://img.shields.io/dub/dw/vibe-d.svg
DUB: https://img.shields.io/dub/dm/vibe-d/latest.svg
DUB: https://img.shields.io/dub/dt/vibe-d/0.7.23.svg
Package Control: https://img.shields.io/packagecontrol/dm/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dw/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dd/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dt/Package%20Control.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/d/nimelepbpejjlbmoobocpfnjhihnpked.svg
Website: https://img.shields.io/website-up-down-green-red/http/shields.io.svg
CocoaPods: https://img.shields.io/cocoapods/dt/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/dm/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/dw/AFNetworking.svg

Version

- + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - + + - - + + - + - + - - + + - - + + - - + + + + + + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + +
npm: https://img.shields.io/npm/v/npm.svg
npm (scoped): https://img.shields.io/npm/v/@cycle/core.svg
node: https://img.shields.io/node/v/gh-badges.svg
PyPI: https://img.shields.io/pypi/v/nine.svg
Gem: https://img.shields.io/gem/v/formatador.svg
Hackage: https://img.shields.io/hackage/v/lens.svg
Crates.io: https://img.shields.io/crates/v/rustc-serialize.svg
Packagist: https://img.shields.io/packagist/v/symfony/symfony.svg
Packagist Pre Release: https://img.shields.io/packagist/vpre/symfony/symfony.svg
Bintray: https://img.shields.io/bintray/v/asciidoctor/maven/asciidoctorj.svg
Clojars: https://img.shields.io/clojars/v/prismic.svg
CocoaPods: https://img.shields.io/cocoapods/v/AFNetworking.svg
Bower: https://img.shields.io/bower/v/bootstrap.svg
Pub: https://img.shields.io/pub/v/box2d.svg
Hex.pm: https://img.shields.io/hexpm/v/plug.svg
GitHub tag:
GitHub tag: https://img.shields.io/github/tag/strongloop/express.svg
GitHub release:
GitHub release: https://img.shields.io/github/release/qubyte/rubidium.svg
GitHub commits:
GitHub commits: https://img.shields.io/github/commits-since/SubtitleEdit/subtitleedit/3.4.7.svg
Chef cookbook: https://img.shields.io/cookbook/v/chef-sugar.svg
NuGet: https://img.shields.io/nuget/v/Nuget.Core.svg
NuGet Pre Release: https://img.shields.io/nuget/vpre/Nuget.Core.svghttps://img.shields.io/nuget/vpre/Microsoft.AspNet.Mvc.svg
MyGet: https://img.shields.io/myget/yolodev/v/FSharpSupport.svghttps://img.shields.io/myget/mongodb/v/MongoDB.Driver.Core.svg
MyGet Pre Release: https://img.shields.io/myget/yolodev/vpre/FSharpSupport.svghttps://img.shields.io/myget/yolodev/vpre/YoloDev.Dnx.FSharp.svg
MyGet tenant: https://img.shields.io/dotnet.myget/dotnet-coreclr/v/Microsoft.DotNet.CoreCLR.svg
Chocolatey: https://img.shields.io/chocolatey/v/git.svg
Puppet Forge: https://img.shields.io/puppetforge/v/vStone/percona.svg
Maven Central: https://img.shields.io/maven-central/v/org.apache.maven/apache-maven.svg
WordPress plugin: https://img.shields.io/wordpress/plugin/v/akismet.svg
WordPress: https://img.shields.io/wordpress/v/akismet.svg
apm: https://img.shields.io/apm/v/vim-mode.svg
CPAN: https://img.shields.io/cpan/v/Config-Augeas.svg
CTAN: https://img.shields.io/ctan/v/tex.svg
DUB: https://img.shields.io/dub/v/vibe-d.svg
AUR: https://img.shields.io/aur/version/yaourt.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/v/nimelepbpejjlbmoobocpfnjhihnpked.svg
homebrew: https://img.shields.io/homebrew/v/cake.svg

Social

- - + + - - + + - - + + - - + + - + + + + +
GitHub forks:
GitHub forks: https://img.shields.io/github/forks/badges/shields.svg?style=social&label=Fork
GitHub stars:
GitHub stars: https://img.shields.io/github/stars/badges/shields.svg?style=social&label=Star
GitHub watchers:
GitHub watchers: https://img.shields.io/github/watchers/badges/shields.svg?style=social&label=Watch
GitHub followers:
GitHub followers: https://img.shields.io/github/followers/espadrine.svg?style=social&label=Follow
Twitter URL: https://img.shields.io/twitter/url/http/shields.io.svg?style=social
Twitter Follow: https://img.shields.io/twitter/follow/shields_io.svg?style=social&label=Follow

Miscellaneous

- - - + + + + + + + - + + + + + - + - + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Gratipay: https://img.shields.io/gratipay/JSFiddle.svg
Gratipay User: https://img.shields.io/gratipay/user/dougwilson.svg
Gratipay Team: https://img.shields.io/gratipay/team/shields.svg
Bountysource: https://img.shields.io/bountysource/team/mozilla-core/activity.svg
Beerpay: https://img.shields.io/beerpay/hashdog/scrapfy-chrome-extension.svg
Code Climate: https://img.shields.io/codeclimate/github/kabisaict/flow.svg
Code Climate: https://img.shields.io/codeclimate/coverage/github/triAGENS/ashikawa-core.svg
Code Climate: https://img.shields.io/codeclimate/issues/github/me-and/mdf.svg
bitHound: https://img.shields.io/bithound/code/github/rexxars/sse-channel.svg
Gemnasium: https://img.shields.io/gemnasium/mathiasbynens/he.svg
Hackage-Deps: https://img.shields.io/hackage-deps/v/lens.svg
Crates.io: https://img.shields.io/crates/l/rustc-serialize.svg
Requires.io: https://img.shields.io/requires/github/celery/celery.svg
VersionEye: https://img.shields.io/versioneye/d/ruby/rails.svg
Packagist: https://img.shields.io/packagist/l/doctrine/orm.svg
npm: https://img.shields.io/npm/l/express.svg
apm: https://img.shields.io/apm/l/vim-mode.svg
Bower: https://img.shields.io/bower/l/bootstrap.svg
PyPI: https://img.shields.io/pypi/l/Django.svg
PyPI: https://img.shields.io/pypi/wheel/Django.svg
PyPI: https://img.shields.io/pypi/format/Django.svg
PyPI: https://img.shields.io/pypi/pyversions/Django.svg
PyPI: https://img.shields.io/pypi/implementation/Django.svg
PyPI: https://img.shields.io/pypi/status/Django.svg
Hex.pm: https://img.shields.io/hexpm/l/plug.svg
CocoaPods: https://img.shields.io/cocoapods/l/AFNetworking.svg
CPAN: https://img.shields.io/cpan/l/Config-Augeas.svg
CTAN: https://img.shields.io/ctan/l/tex.svg
Wheelmap: https://img.shields.io/wheelmap/a/2323004600.svg
GitHub issues:
GitHub issues: https://img.shields.io/github/issues/badges/shields.svg
https://img.shields.io/github/issues-raw/badges/shields.svg
GitHub license:
GitHub closed issues: https://img.shields.io/github/issues-closed/badges/shields.svg
https://img.shields.io/github/issues-closed-raw/badges/shields.svg
label:https://img.shields.io/github/issues-raw/badges/shields/website.svg
GitHub pull requests: https://img.shields.io/github/issues-pr/cdnjs/cdnjs.svg
https://img.shields.io/github/issues-pr-raw/cdnjs/cdnjs.svg
GitHub closed pull requests: https://img.shields.io/github/issues-pr-closed/cdnjs/cdnjs.svg
https://img.shields.io/github/issues-pr-closed-raw/cdnjs/cdnjs.svg
GitHub contributors: https://img.shields.io/github/contributors/cdnjs/cdnjs.svg
license: https://img.shields.io/github/license/mashape/apistatus.svg
Bitbucket issues: https://img.shields.io/bitbucket/issues/atlassian/python-bitbucket.svg
https://img.shields.io/bitbucket/issues-raw/atlassian/python-bitbucket.svg
WordPress rating: https://img.shields.io/wordpress/plugin/r/akismet.svg
Codacy: https://img.shields.io/codacy/e27821fb6289410b8f58338c7e0bc686.svg
Codacy grade: https://img.shields.io/codacy/grade/e27821fb6289410b8f58338c7e0bc686.svg
Codacy branch grade: https://img.shields.io/codacy/grade/e27821fb6289410b8f58338c7e0bc686/master.svg
Codacy coverage: https://img.shields.io/codacy/coverage/c44df2d9c89a4809896914fd1a40bedd.svg
Codacy branch coverage: https://img.shields.io/codacy/coverage/c44df2d9c89a4809896914fd1a40bedd/master.svg
Libscore: https://img.shields.io/libscore/s/jQuery.svg
Talk.ai: https://img.shields.io/talk/t/9c81ff703b.svg
Puppet Forge: https://img.shields.io/puppetforge/e/camptocamp/openssl.svg
Puppet Forge: https://img.shields.io/puppetforge/f/camptocamp/openssl.svg
Puppet Forge: https://img.shields.io/puppetforge/rc/camptocamp.svg
Puppet Forge: https://img.shields.io/puppetforge/mc/camptocamp.svg
Gems: https://img.shields.io/gem/u/raphink.svg
Gems: https://img.shields.io/gem/rt/puppet.svg
Gems: https://img.shields.io/gem/rd/facter.svg
DUB: https://img.shields.io/dub/l/vibe-d.svg
Docker Stars: https://img.shields.io/docker/stars/_/ubuntu.svg
Docker Pulls: https://img.shields.io/docker/pulls/mashape/kong.svg
Docker Automated build https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg
ImageLayers Size: https://img.shields.io/imagelayers/image-size/_/ubuntu/latest.svg
ImageLayers Layers: https://img.shields.io/imagelayers/layers/_/ubuntu/latest.svg
Gitter: https://img.shields.io/gitter/room/nwjs/nw.js.svg
JIRA issue: https://img.shields.io/jira/issue/https/issues.apache.org/jira/KAFKA-2896.svg
Maintenance: https://img.shields.io/maintenance/yes/2016.svg
AUR: https://img.shields.io/aur/license/yaourt.svg
Waffle.io: https://img.shields.io/waffle/label/evancohen/smart-mirror/in%20progress.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/price/nimelepbpejjlbmoobocpfnjhihnpked.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/rating/nimelepbpejjlbmoobocpfnjhihnpked.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/rating-count/nimelepbpejjlbmoobocpfnjhihnpked.svg
AUR: https://img.shields.io/aur/votes/yaourt.svg

Longer Miscellaneous

- + - + - + - + + + + + + + + + - + + + + + + + + + - + - + - - - + + + - - - + + +
David: https://img.shields.io/david/strongloop/express.svg
David: https://img.shields.io/david/dev/strongloop/express.svg
David: https://img.shields.io/david/optional/elnounch/byebye.svg
David: https://img.shields.io/david/peer/webcomponents/generator-element.svg
bitHound: https://img.shields.io/bithound/dependencies/github/rexxars/sse-channel.svg
bitHound: https://img.shields.io/bithound/devDependencies/github/rexxars/sse-channel.svg
CocoaPods: https://img.shields.io/cocoapods/at/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/aw/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/p/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/metrics/doc-percent/AFNetworking.svg
Ansible Role: https://img.shields.io/ansible/role/3078.svg
Libraries.io for releases: https://img.shields.io/librariesio/release/hex/phoenix/1.0.3.svg
StackExchange: https://img.shields.io/stackexchange/tex/r/951.svg
Libraries.io for GitHub: https://img.shields.io/librariesio/github/phoenixframework/phoenix.svg
StackExchange: https://img.shields.io/stackexchange/stackoverflow/t/augeas.svg
@@ -763,15 +958,15 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable

-brightgreen -green -yellowgreen -yellow -orange -red -lightgrey -blue -ff69b4 +brightgreen +green +yellowgreen +yellow +orange +red +lightgrey +blue +ff69b4

Styles

@@ -780,19 +975,19 @@ The following styles are available (flat is the default as of Feb 1st 2015):

- + - + - + - +
https://img.shields.io/badge/style-plastic-green.svg?style=plastic
https://img.shields.io/badge/style-flat-green.svg?style=flat
https://img.shields.io/badge/style-flat--squared-green.svg?style=flat-square
https://img.shields.io/badge/style-social-green.svg?style=social
@@ -810,6 +1005,8 @@ Here are a few other parameters you can use: ?link=http://abc.xyz&link=http://shields.io Specify what clicking on the left/right of a badge should do (esp. for social badge style) + ?maxAge=3600 + Set the HTTP cache lifetime in secs

@@ -824,10 +1021,8 @@ Tell your favorite badge service to use it!
And tell us, we might be able to bring it to you anyway!

- - -Donate to us! +Follow @shields_io +Donate to us! @@ -837,127 +1032,11 @@ And tell us, we might be able to bring it to you anyway! is where the current server got started.

-

Contributors

- - - espadrine - - - mathiasbynens - - - olivierlacan - - - nathany - - - whit537 - - - kura - - - maxogden - - - seanlinsley - - - alrra - - - jublo - - - g105b - - - Daniel15 - -
- - sebmck - - - avanderhoorn - - - stefanjudis - - - jmalloc - - - ezzatron - - - brettcannon - - - CodeBlock - - - Mikulas - - - segiddins - - - SonicHedgehog - - - akashivskyy - - - akashivskyy - -
- - rmasters - - - cainus - - - jbowes - - - rafalchmiel - - - fjcaetano - - - hughsk - - - qubyte - - - khellang - - - nitram509 - - - raphink - - - montanaflynn - - - PeterDaveHello - -
- - sagiegurari -

:wq

-
+

@@ -972,9 +1051,83 @@ is where the current server got started.

reStructuredText +

+ + @@ -1082,14 +1235,6 @@ function selectNode(e) { }; copyMarkdown.addEventListener('click', selectNode); copyreStructuredText.addEventListener('click', selectNode); -function autoselectCode() { - var codes = document.querySelectorAll('table.badge code'); - for (var i = 0; i < codes.length; i++) { - codes[i].addEventListener('click', selectNode); - } -} - -document.addEventListener('DOMContentLoaded', autoselectCode); // Markup copier dialog // @@ -1098,10 +1243,14 @@ function markupDialogInit() { var trs = document.querySelectorAll('table.badge tr'); for (var i = 0; i < trs.length; i++) { var tr = trs[i]; + var title = tr.querySelector('th'); var target = tr.querySelector('img'); - if (target) { - target.addEventListener('click', makeMarkupDialogListener(tr)); - } + var code = tr.querySelector('code'); + // Markup dialog listener. + var mdl = makeMarkupDialogListener(tr); + if (title != null) { title.addEventListener('click', mdl); } + if (target != null) { target.addEventListener('click', mdl); } + if (code != null) { code.addEventListener('click', mdl); } } } function makeMarkupDialogListener(tr) { @@ -1119,8 +1268,18 @@ function markupDialog(tr) { var trimg = tr.querySelector('img').src; var th = tr.firstElementChild; var link = th.dataset.link? th.dataset.link: ''; + // Remove the ?maxAge parameter from the query string. + link.replace(/[\?&]maxAge=\d+$|maxAge=\d+&/, ''); copyForm.img.value = trimg; copyForm.url.value = link; + // Insert documentation. + var doc = th.dataset.doc? th.dataset.doc: ''; + var docelt = document.getElementById(doc); + if (docelt != null) { + copyDoc.innerHTML = '

Documentation

' + docelt.innerHTML; + } else { + copyDoc.innerHTML = ''; + } // Set up the input listeners. copyForm.url.removeEventListener('input', copyFormUrlEventListener); copyForm.img.removeEventListener('input', copyFormUrlEventListener); diff --git a/lib/github-auth.js b/lib/github-auth.js new file mode 100644 index 0000000..52b9bbc --- /dev/null +++ b/lib/github-auth.js @@ -0,0 +1,253 @@ +var querystring = require('querystring'); +var request = require('request'); +var autosave = require('json-autosave'); +var serverSecrets; +try { + // Everything that cannot be checked in but is useful server-side + // is stored in this JSON data. + serverSecrets = require('../secret.json'); +} catch(e) {} +var githubUserTokens; +var githubUserTokensFile = '.github-user-tokens.json'; +autosave(githubUserTokensFile, {data:[]}).then(function(f) { + githubUserTokens = f; + for (var i = 0; i < githubUserTokens.data.length; i++) { + addGithubToken(githubUserTokens.data[i]); + } +}).catch(function(e) { console.error('Could not create ' + githubUserTokensFile); }); + +function setRoutes(server) { + server.route(/^\/github-auth$/, function(data, match, end, ask) { + if (!(serverSecrets && serverSecrets.gh_client_id)) { + return end('This server is missing GitHub client secrets.'); + } + var query = querystring.stringify({ + client_id: serverSecrets.gh_client_id, + redirect_uri: 'https://img.shields.io/github-auth/done', + }); + ask.res.statusCode = 302; // Found. + ask.res.setHeader('Location', 'https://github.com/login/oauth/authorize?' + query); + end(''); + }); + + server.route(/^\/github-auth\/done$/, function(data, match, end, ask) { + if (!(serverSecrets && serverSecrets.gh_client_id && serverSecrets.gh_client_secret)) { + return end('This server is missing GitHub client secrets.'); + } + if (!data.code) { + return end('GitHub OAuth authentication failed to provide a code.'); + } + var options = { + url: 'https://github.com/login/oauth/access_token', + headers: { + 'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'User-Agent': 'Shields.io', + }, + form: querystring.stringify({ + client_id: serverSecrets.gh_client_id, + client_secret: serverSecrets.gh_client_secret, + code: data.code, + }), + method: 'POST', + }; + request(options, function(err, res, body) { + if (err != null) { return end('The connection to GitHub failed.'); } + try { + var content = querystring.parse(body); + } catch(e) { return end('The GitHub OAuth token could not be parsed.'); } + var token = content.access_token; + if (!token) { + return end('The GitHub OAuth process did not return a user token.'); + } + console.log('GitHub OAuth: ' + token); + + ask.res.setHeader('Content-Type', 'text/html'); + end('

Shields.io has received your app-specific GitHub user token. ' + + 'You can revoke it by going to ' + + 'GitHub.

' + + '

Until you do, you have now increased the rate limit for GitHub ' + + 'requests going through Shields.io. GitHub-related badges are ' + + 'therefore more robust.

' + + '

Thanks for contributing to a smoother experience for ' + + 'everyone!

' + + '

Back to the website

'); + + sendTokenToAllServers(token) + .catch(function(e) { + console.error('GitHub user token transmission failed:', e); + }); + }); + }); + + server.route(/^\/github-auth\/add-token$/, function(data, match, end, ask) { + console.log('GitHub add token called with', JSON.stringify(data)); + if (data.shieldsSecret !== serverSecrets.shieldsSecret) { + // An unknown entity tries to connect. Let the connection linger for a minute. + return setTimeout(function() { end('Invalid secret.'); }, 60000); + } + addGithubToken(data.token); + end('Thanks!'); + }); +}; + +function sendTokenToAllServers(token) { + var ips = serverSecrets.shieldsIps; + return Promise.all(ips.map(function(ip) { + return new Promise(function(resolve, reject) { + var options = { + url: 'https://' + ip + '/github-auth/add-token', + method: 'POST', + form: { + shieldsSecret: serverSecrets.shieldsSecret, + token: token, + }, + // We target servers by IP, and we use HTTPS. Assuming that + // 1. Internet routers aren't hacked, and + // 2. We don't unknowingly lose our IP to someone else, + // we're not leaking people's and our information. + // (If we did, it would have no impact, as we only ask for a token, + // no GitHub scope. The malicious entity would only be able to use + // our rate limit pool.) + // FIXME: use letsencrypt. + strictSSL: false, + }; + request(options, function(err, res, body) { + if (err != null) { return reject('Posting the GitHub user token failed: ' + err.stack); } + resolve(); + }); + }); + })); +} + +// Track rate limit requests remaining. + +// Ideally, we would want priority queues here. +var reqRemaining = new Map(); // From token to requests remaining. +var reqReset = new Map(); // From token to timestamp. + +// token: client token as a string. +// reqs: number of requests remaining. +// reset: timestamp when the number of remaining requests is reset. +function setReqRemaining(token, reqs, reset) { + reqRemaining.set(token, reqs); + reqReset.set(token, reset); +} + +function rmReqRemaining(token) { + reqRemaining.delete(token); + reqReset.delete(token); +} + +function utcEpochSeconds() { + return ((Date.now() / 1000) >>> 0); +} + +var userTokenRateLimit = 12500; + +// Return false if the token cannot reasonably be expected to perform +// a GitHub request. +function isTokenUsable(token, now) { + var reqs = reqRemaining.get(token); + var reset = reqReset.get(token); + // We don't want to empty more than 3/4 of a user's rate limit. + var hasRemainingReqs = reqs > (userTokenRateLimit / 4); + var isBeyondRateLimitReset = reset < now; + return hasRemainingReqs || isBeyondRateLimitReset; +} + +// Return a list of tokens (as strings) which can be used for a GitHub request, +// with a reasonable chance that the request will succeed. +function usableTokens() { + var now = utcEpochSeconds(); + return githubUserTokens.data.filter(function(token) { + return isTokenUsable(token, now); + }); +} + +// Retrieve a user token if there is one for which we believe there are requests +// remaining. Return undefined if we could not find one. +function getReqRemainingToken() { + // Go through the user tokens. + // Among usable ones, use the one with the highest number of remaining + // requests. + var tokens = usableTokens(); + var highestReq = -1; + var highestToken; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + var reqs = reqRemaining.get(token); + if (reqs > highestReq) { + highestReq = reqs; + highestToken = token; + } + } + return highestToken; +} + +function addGithubToken(token) { + // A reset date of 0 has to be in the past. + setReqRemaining(token, userTokenRateLimit, 0); + // Insert it only if it is not registered yet. + if (githubUserTokens.data.indexOf(token) === -1) { + githubUserTokens.data.push(token); + } +} + +function rmGithubToken(token) { + rmReqRemaining(token); + // Remove it only if it is in there. + var idx = githubUserTokens.data.indexOf(token); + if (idx >= 0) { + githubUserTokens.data.splice(idx, 1); + } +} + +// Personal tokens allow access to GitHub private repositories. +// You can manage your personal GitHub token at +// . +if (serverSecrets && serverSecrets.gh_token) { + addGithubToken(serverSecrets.gh_token); +} + +// Act like request(), but tweak headers and query to avoid hitting a rate +// limit. +function githubRequest(request, url, query, cb) { + query = query || {}; + // A special User-Agent is required: + // http://developer.github.com/v3/#user-agent-required + var headers = { + 'User-Agent': 'Shields.io', + 'Accept': 'application/vnd.github.v3+json', + }; + var githubToken = getReqRemainingToken(); + + if (githubToken != null) { + // Typically, GitHub user tokens grants us 12500 req/hour. + headers['Authorization'] = 'token ' + githubToken; + } else if (serverSecrets && serverSecrets.gh_client_id) { + // Using our OAuth App secret grants us 5000 req/hour + // instead of the standard 60 req/hour. + query.client_id = serverSecrets.gh_client_id; + query.client_secret = serverSecrets.gh_client_secret; + } + + var qs = querystring.stringify(query); + if (qs) { url += '?' + qs; } + request(url, {headers: headers}, function(err, res, buffer) { + if (githubToken != null) { + if (res.statusCode === 401) { // Unauthorized. + rmGithubToken(githubToken); + } else { + var remaining = +res.headers['x-ratelimit-remaining']; + // reset is in UTC epoch seconds. + var reset = +res.headers['x-ratelimit-reset']; + setReqRemaining(githubToken, remaining, reset); + if (remaining === 0) { return; } // Hope for the best in the cache. + } + } + cb(err, res, buffer); + }); +} + +exports.setRoutes = setRoutes; +exports.request = githubRequest; diff --git a/logo/bithound.svg b/logo/bithound.svg new file mode 100644 index 0000000..7fb554d --- /dev/null +++ b/logo/bithound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/measure-text.js b/measure-text.js new file mode 100644 index 0000000..f96c204 --- /dev/null +++ b/measure-text.js @@ -0,0 +1,19 @@ +'use strict'; + +var path = require('path'); +var fs = require('fs'); +var PDFDocument = require('pdfkit'); + +var doc = (new PDFDocument({size:'A4', layout:'landscape'})); +try { + doc = doc.font(path.join(__dirname, 'Verdana.ttf')); +} catch (ex) { + doc = doc.font('Helvetica-Bold') + console.warn('Could not load font file "Verdana.ttf", text widths will therefore be approximate', ex); +} +doc = doc.fontSize(11); + +module.exports = measure; +function measure(str) { + return doc.widthOfString(str); +} \ No newline at end of file diff --git a/package.json b/package.json index c4ce894..a41fef4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gh-badges", - "version": "1.1.2", + "version": "1.2.2", "description": "GitHub badges implemented in SVG.", "keywords": ["GitHub", "badge", "SVG", "image"], "homepage": "http://shields.io", @@ -18,35 +18,30 @@ "dependencies": { "dot": "~1.0.3", "svgo": "~0.5.1", - "canvas": "~1.1.2", - "phantomjs": "~1.9.2-6", - "es6-promise": "~2.1.0", + "pdfkit": "~0.7.1", + "phantomjs-prebuilt": "~2.1.7", "request": "~2.55.0", "redis": "~1.0.0", - "camp": "~15.5.5", + "camp": "~16.2.2", "semver": "~4.3.3", "bower": "~1.4.1", - "promise": "~7.0.0" - }, - "devDependencies": { - "ass": "~0.0.6", - "should": "~3.0.0", - "mocha": "~1.14.0" + "chrome-web-store-item-property": "^1.1.2", + "json-autosave": "~1.1.1" }, "scripts": { - "test": "mocha -R spec test.js" + "test": "node test/test.js" }, "bin": { "badge": "./gh-badge.js" }, "files": [ "badge.js", "README.md", "gh-badge.js", + "measure-text.js", "templates", "svg-to-img.js", "colorscheme.json", "phantomjs-svg2png.js", "lru-cache.js", "logo" - ], - "engines": { "node": "0.10.x" } + ] } diff --git a/server.js b/server.js index baf901d..a58e1d7 100644 --- a/server.js +++ b/server.js @@ -18,6 +18,7 @@ var LruCache = require('./lru-cache.js'); var badge = require('./badge.js'); var svg2img = require('./svg-to-img.js'); var loadLogos = require('./load-logos.js'); +var githubAuth = require('./lib/github-auth.js'); var querystring = require('querystring'); var serverSecrets; try { @@ -25,6 +26,10 @@ try { // is stored in this JSON data. serverSecrets = require('./secret.json'); } catch(e) { console.error('No secret data (secret.json, see server.js):', e); } +if (serverSecrets && serverSecrets.gh_client_id) { + githubAuth.setRoutes(camp); +} + var semver = require('semver'); var serverStartTime = new Date((new Date()).toGMTString()); @@ -170,8 +175,12 @@ vendorDomain.on('error', function(err) { function cache(f) { return function getRequest(data, match, end, ask) { - // Cache management - no cache, so it won't be cached by GitHub's CDN. - ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + if (data.maxAge !== undefined && /^[0-9]+$/.test(data.maxAge)) { + ask.res.setHeader('Cache-Control', 'max-age=' + data.maxAge); + } else { + // Cache management - no cache, so it won't be cached by GitHub's CDN. + ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + } var reqTime = new Date(); var date = (reqTime).toGMTString(); ask.res.setHeader('Expires', date); // Proxies, GitHub, see #221. @@ -191,7 +200,6 @@ function cache(f) { var cachedVersionSent = false; if (cached !== undefined) { // A request was made not long ago. - var interval = 30000; // In milliseconds. var tooSoon = (+reqTime - cached.time) < cached.interval; if (tooSoon || (cached.dataChange / cached.reqs <= freqRatioMax)) { badge(cached.data.badgeData, makeSend(cached.data.format, ask.res, end)); @@ -211,6 +219,7 @@ function cache(f) { badge(cached.badgeData, makeSend(cached.format, ask.res, end)); return; } + ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); var badgeData = getBadgeData('vendor', data); badgeData.text[1] = 'unresponsive'; var extension; @@ -285,6 +294,64 @@ camp.notfound(/.*/, function(query, match, end, request) { // Vendors. +// JIRA issue integration +camp.route(/^\/jira\/issue\/(http(?:s)?)\/(.+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function (data, match, sendBadge, request) { + var protocol = match[1]; // eg, https + var host = match[2]; // eg, issues.apache.org/jira + var issueKey = match[3]; // eg, KAFKA-2896 + var format = match[4]; + + var options = { + method: 'GET', + json: true, + uri: protocol + '://' + host + '/rest/api/2/issue/' + + encodeURIComponent(issueKey) + }; + if (serverSecrets && serverSecrets.jira_username) { + options.auth = { + user: serverSecrets.jira_username, + pass: serverSecrets.jira_password + }; + } + + // map JIRA color names to closest shields color schemes + var colorMap = { + 'medium-gray': 'lightgrey', + 'green': 'green', + 'yellow': 'yellow', + 'brown': 'orange', + 'warm-red': 'red', + 'blue-gray': 'blue' + }; + + var badgeData = getBadgeData(issueKey, data); + request(options, function (err, res, json) { + if (err !== null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var jiraIssue = json; + if (jiraIssue.fields && jiraIssue.fields.status) { + if (jiraIssue.fields.status.name) { + badgeData.text[1] = jiraIssue.fields.status.name; // e.g. "In Development" + } + if (jiraIssue.fields.status.statusCategory) { + badgeData.colorscheme = colorMap[jiraIssue.fields.status.statusCategory.colorName] || 'lightgrey'; + } + } else { + badgeData.text[1] = 'invalid'; + } + sendBadge(format, badgeData); + } catch (e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Travis integration camp.route(/^\/travis(-ci)?\/([^\/]+\/[^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -308,15 +375,15 @@ cache(function(data, match, sendBadge, request) { return; } try { - var res = res.headers['content-disposition'] - .match(/filename="(.+)\.svg"/)[1]; - badgeData.text[1] = res; - if (res === 'passing') { + var state = res.headers['content-disposition'] + .match(/filename="(.+)\.svg"/)[1]; + badgeData.text[1] = state; + if (state === 'passing') { badgeData.colorscheme = 'brightgreen'; - } else if (res === 'failing') { + } else if (state === 'failing') { badgeData.colorscheme = 'red'; } else { - badgeData.text[1] = res; + badgeData.text[1] = state; } sendBadge(format, badgeData); @@ -328,35 +395,95 @@ cache(function(data, match, sendBadge, request) { })); // Shippable integration -camp.route(/^\/shippable?\/([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { +camp.route(/^\/shippable\/([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, +cache(function (data, match, sendBadge, request) { + var defaultOpts = { + colorA: '#555555', + successLabel: 'passing', + successColor: '#44CC11', + failLabel: 'failing', + failColor: '#DC5F59', + cancelledLabel: 'cancelled', + cancelledColor: '#6BAFBD', + unstableLabel: 'unstable', + unstableColor: '#CEA61B', + pendingLabel: 'pending', + pendingColor: '#5183A0', + skippedLabel: 'skipped', + skippedColor: '#F8A97D', + noBuildLabel: 'none', + noBuildColor: '#A1ABAB', + inaccessibleLabel: 'inaccessible', + inaccessibleColor: '#A1ABAB' + }; + + var badgeData = getBadgeData('build', data); + delete badgeData.colorscheme; + + // overwrite the default options if present in query parameters + Object.keys(defaultOpts).forEach( + function (key) { + defaultOpts[key] = data[key] || defaultOpts[key]; + } + ); + + badgeData.colorA = defaultOpts.colorA; + badgeData.colorB = defaultOpts.noBuildColor; + var project = match[1]; // eg, 54d119db5ab6cc13528ab183 var branch = match[2]; var format = match[3]; var url = 'https://api.shippable.com/projects/' + project + '/badge'; + if (branch != null) { - url += '?branchName=' + branch; + url += '?branch=' + branch; } - var badgeData = getBadgeData('build', data); - fetchFromSvg(request, url, function(err, res) { + + fetchFromSvg(request, url, function (err, res) { if (err != null) { - badgeData.text[1] = 'inaccessible'; + badgeData.text[1] = defaultOpts.inaccessibleLabel; sendBadge(format, badgeData); return; } + try { - badgeData.text[1] = res; - if (res === 'shippable') { - badgeData.colorscheme = 'brightgreen'; - } else if (res === 'unshippable') { - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = res; + switch (res) { + case 'none': + badgeData.text[1] = defaultOpts.noBuildLabel; + badgeData.colorB = defaultOpts.noBuildColor; + break; + case 'shippable': + badgeData.text[1] = defaultOpts.successLabel; + badgeData.colorB = defaultOpts.successColor; + break; + case 'failed': + badgeData.text[1] = defaultOpts.failLabel; + badgeData.colorB = defaultOpts.failColor; + break; + case 'cancelled': + badgeData.text[1] = defaultOpts.cancelledLabel; + badgeData.colorB = defaultOpts.cancelledColor; + break; + case 'pending': + badgeData.text[1] = defaultOpts.pendingLabel; + badgeData.colorB = defaultOpts.pendingColor; + break; + case 'skipped': + badgeData.text[1] = defaultOpts.skippedLabel; + badgeData.colorB = defaultOpts.skippedColor; + break; + case 'unstable': + badgeData.text[1] = defaultOpts.unstableLabel; + badgeData.colorB = defaultOpts.unstableColor; + break; + default: + badgeData.text[1] = 'invalid'; + badgeData.colorB = defaultOpts.noBuildColor; } sendBadge(format, badgeData); - - } catch(e) { + } catch (e) { badgeData.text[1] = 'invalid'; + badgeData.colorB = defaultOpts.noBuildColor; sendBadge(format, badgeData); } }); @@ -385,7 +512,7 @@ cache(function(data, match, sendBadge, request) { if (build.status === 'finished') { if (build.result === 'passed') { badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = build.result; + badgeData.text[1] = 'passing'; } else { badgeData.colorscheme = 'red'; badgeData.text[1] = build.result; @@ -426,7 +553,7 @@ cache(function(data, match, sendBadge, request) { if (build.status === 'finished') { if (build.result === 'passed') { badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = build.result; + badgeData.text[1] = 'passing'; } else { badgeData.colorscheme = 'red'; badgeData.text[1] = build.result; @@ -457,7 +584,7 @@ cache(function (data, match, sendBadge, request) { process: function (data, badgeData) { downloads = data.crate? data.crate.downloads: data.version.downloads; version = data.version && data.version.num; - badgeData.text[1] = metric(downloads) + (version? ' version ' + version: ' total'); + badgeData.text[1] = metric(downloads) + (version? ' version ' + version: ''); badgeData.colorscheme = downloadCountColor(downloads); } }, @@ -566,6 +693,7 @@ function teamcity_badge(url, buildId, advanced, format, data, sendBadge) { badgeData.text[1] = (data.status || '').toLowerCase(); if (data.status === 'SUCCESS') { badgeData.colorscheme = 'brightgreen'; + badgeData.text[1] = 'passing'; } else { badgeData.colorscheme = 'red'; } @@ -765,6 +893,7 @@ cache(function(data, match, sendBadge, request) { if (data.message === 'passed') { badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing'; } else if (/^passed .* new defects$/.test(data.message)) { badgeData.colorscheme = 'yellow'; } else if (data.message === 'pending') { @@ -816,8 +945,8 @@ cache(function(data, match, sendBadge, request) { return; } try { - badgeData = JSON.parse(buffer); - sendBadge(format, badgeData); + var data = JSON.parse(buffer); + sendBadge(format, data); } catch(e) { badgeData.text[1] = 'invalid'; sendBadge(format, badgeData); @@ -827,11 +956,14 @@ cache(function(data, match, sendBadge, request) { })); // Gratipay integration. -camp.route(/^\/(?:gittip|gratipay(?:\/user)?)\/(.*)\.(svg|png|gif|jpg|json)$/, +camp.route(/^\/(?:gittip|gratipay(\/user|\/team)?)\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, `dougwilson`. - var format = match[2]; - var apiUrl = 'https://www.gratipay.com/~' + user + '/public.json'; + var type = match[1]; // eg, `user`. + var user = match[2]; // eg, `dougwilson`. + var format = match[3]; + if (type === '') { type = '/user'; } + if (type === '/user') { user = '~' + user; } + var apiUrl = 'https://gratipay.com/' + user + '/public.json'; var badgeData = getBadgeData('tips', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.gratipay; @@ -846,13 +978,12 @@ cache(function(data, match, sendBadge, request) { var data = JSON.parse(buffer); var receiving = data.receiving || data.taking; if (receiving) { - var money = parseInt(receiving); - badgeData.text[1] = '$' + metric(money) + '/week'; - if (money === 0) { + badgeData.text[1] = '$' + metric(receiving) + '/week'; + if (receiving === 0) { badgeData.colorscheme = 'red'; - } else if (money < 10) { + } else if (receiving < 10) { badgeData.colorscheme = 'yellow'; - } else if (money < 100) { + } else if (receiving < 100) { badgeData.colorscheme = 'green'; } else { badgeData.colorscheme = 'brightgreen'; @@ -884,7 +1015,7 @@ cache(function(data, match, sendBadge, request) { } try { var data = JSON.parse(buffer); - badgeData.text[1] = metric(+data.count); + badgeData.text[1] = metric(+data.count[data.count.length-1]); badgeData.colorscheme = 'blue'; sendBadge(format, badgeData); } catch(e) { @@ -1070,7 +1201,7 @@ cache(function(data, match, sendBadge, request) { break; case 't': var downloads = data.package.downloads.total; - badgeData.text[1] = metric(downloads) + ' total'; + badgeData.text[1] = metric(downloads); break; } badgeData.colorscheme = downloadCountColor(downloads); @@ -1211,6 +1342,71 @@ cache(function(data, match, sendBadge, request) { }); })); +// Package Control integration. +camp.route(/^\/packagecontrol\/(dm|dw|dd|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var info = match[1]; // either `dm`, `dw`, `dd` or dt`. + var userRepo = match[2]; // eg, `Package%20Control`. + var format = match[3]; + var apiUrl = 'https://packagecontrol.io/packages/' + userRepo + '.json'; + var badgeData = getBadgeData('downloads', data); + request(apiUrl, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + } + try { + var data = JSON.parse(buffer); + var downloads = 0; + switch (info.charAt(1)) { + case 'm': + // daily downloads are separated by Operating System + var platforms = data.installs.daily.data; + platforms.forEach(function(platform) { + // loop through the first 30 days or 1 month + for (var i = 0; i < 30; i++) { + // add the downloads for that day for that platform + downloads += platform.totals[i]; + } + }); + badgeData.text[1] = metric(downloads) + '/month'; + break; + case 'w': + // daily downloads are separated by Operating System + var platforms = data.installs.daily.data; + platforms.forEach(function(platform) { + // loop through the first 7 days or 1 week + for (var i = 0; i < 7; i++) { + // add the downloads for that day for that platform + downloads += platform.totals[i]; + } + }); + badgeData.text[1] = metric(downloads) + '/week'; + break; + case 'd': + // daily downloads are separated by Operating System + var platforms = data.installs.daily.data; + platforms.forEach(function(platform) { + // use the downloads from yesterday + downloads += platform.totals[1]; + }); + badgeData.text[1] = metric(downloads) + '/day'; + break; + case 't': + // all-time downloads are already compiled + downloads = data.installs.total; + badgeData.text[1] = metric(downloads); + break; + } + badgeData.colorscheme = downloadCountColor(downloads); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // npm download integration. camp.route(/^\/npm\/dm\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -1225,25 +1421,24 @@ cache(function(data, match, sendBadge, request) { return; } try { - var monthly = JSON.parse(buffer).downloads; + var monthly = JSON.parse(buffer).downloads || 0; + badgeData.text[1] = metric(monthly) + '/month'; + if (monthly === 0) { + badgeData.colorscheme = 'red'; + } else if (monthly < 10) { + badgeData.colorscheme = 'yellow'; + } else if (monthly < 100) { + badgeData.colorscheme = 'yellowgreen'; + } else if (monthly < 1000) { + badgeData.colorscheme = 'green'; + } else { + badgeData.colorscheme = 'brightgreen'; + } + sendBadge(format, badgeData); } catch(e) { badgeData.text[1] = 'invalid'; sendBadge(format, badgeData); - return; } - badgeData.text[1] = metric(monthly) + '/month'; - if (monthly === 0) { - badgeData.colorscheme = 'red'; - } else if (monthly < 10) { - badgeData.colorscheme = 'yellow'; - } else if (monthly < 100) { - badgeData.colorscheme = 'yellowgreen'; - } else if (monthly < 1000) { - badgeData.colorscheme = 'green'; - } else { - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); }); })); @@ -1264,12 +1459,12 @@ cache(function (data, match, sendBadge, request) { try { var totalDownloads = 0; - var downloads = JSON.parse(buffer).downloads; + var downloads = JSON.parse(buffer).downloads || 0; for (var index = 0; index < downloads.length; index++) { totalDownloads = totalDownloads + downloads[index].downloads; } - badgeData.text[1] = metric(totalDownloads) + ' total'; + badgeData.text[1] = metric(totalDownloads); if (totalDownloads === 0) { badgeData.colorscheme = 'red'; } else { @@ -1288,7 +1483,7 @@ camp.route(/^\/npm\/v\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { var repo = encodeURIComponent(match[1]); // eg, "express" or "@user/express" var format = match[2]; - var apiUrl = 'https://registry.npmjs.org/' + repo + '/latest'; + var apiUrl = 'https://registry.npmjs.org/-/package/' + repo + '/dist-tags'; var badgeData = getBadgeData('npm', data); // Using the Accept header because of this bug: // @@ -1300,7 +1495,7 @@ cache(function(data, match, sendBadge, request) { } try { var data = JSON.parse(buffer); - var version = data.version; + var version = data.latest; var vdata = versionColor(version); badgeData.text[1] = vdata.version; badgeData.colorscheme = vdata.color; @@ -1850,7 +2045,7 @@ cache(function(data, match, sendBadge, request) { break; case 't': var downloads = data.downloads.all; - badgeData.text[1] = metric(downloads) + ' total'; + badgeData.text[1] = metric(downloads); break; } badgeData.colorscheme = downloadCountColor(downloads); @@ -1999,7 +2194,7 @@ cache(function(data, match, sendBadge, request) { } try { var score = res.headers['content-disposition'] - .match(/filename="coverage_(.+)\.png"/)[1]; + .match(/filename=".*coverage_(.+)\.png"/)[1]; if (!score) { badgeData.text[1] = 'malformed'; sendBadge(format, badgeData); @@ -2022,6 +2217,46 @@ cache(function(data, match, sendBadge, request) { }); })); +// Code Climate issues integration +camp.route(/^\/codeclimate\/issues\/(.+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var userRepo = match[1]; // eg, `github/me-and/mdf`. + var format = match[2]; + var options = 'https://codeclimate.com/' + userRepo + '/badges/issue_count.svg' + var badgeData = getBadgeData('issues', data); + request(options, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var count = buffer.match(/>([0-9]+) issues?/)[1]; + if (!count) { + badgeData.text[1] = 'malformed'; + sendBadge(format, badgeData); + return; + } + badgeData.text[1] = count; + if (count == 0) { + badgeData.colorscheme = 'brightgreen'; + } else if (count < 5) { + badgeData.colorscheme = 'green'; + } else if (count < 10) { + badgeData.colorscheme = 'yellowgreen'; + } else if (count < 20) { + badgeData.colorscheme = 'yellow'; + } else { + badgeData.colorscheme = 'red'; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Code Climate integration camp.route(/^\/codeclimate\/(.+)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -2040,7 +2275,7 @@ cache(function(data, match, sendBadge, request) { } try { var statusMatch = res.headers['content-disposition'] - .match(/filename="code_climate-(.+)\.png"/); + .match(/filename=".*code_climate-(.+)\.png"/); if (!statusMatch) { badgeData.text[1] = 'unknown'; sendBadge(format, badgeData); @@ -2080,8 +2315,8 @@ cache(function(data, match, sendBadge, request) { var slashesInRepo = 2; if (repoParts[0] === 'gp') { slashesInRepo = 1; } if ((repoParts.length - 1) > slashesInRepo) { - branch = repoParts[repoParts.length - 1]; - repo = repoParts.slice(0, -1).join('/'); + branch = repoParts.slice(slashesInRepo + 1).join('/'); + repo = repoParts.slice(0, slashesInRepo + 1).join('/'); } var apiUrl = 'https://scrutinizer-ci.com/api/repositories/' + repo; var badgeData = getBadgeData('coverage', data); @@ -2119,8 +2354,8 @@ cache(function(data, match, sendBadge, request) { var slashesInRepo = 2; if (repoParts[0] === 'gp') { slashesInRepo = 1; } if ((repoParts.length - 1) > slashesInRepo) { - branch = repoParts[repoParts.length - 1]; - repo = repoParts.slice(0, -1).join('/'); + branch = repoParts.slice(slashesInRepo + 1).join('/'); + repo = repoParts.slice(0, slashesInRepo + 1).join('/'); } var apiUrl = 'https://scrutinizer-ci.com/api/repositories/' + repo; var badgeData = getBadgeData('build', data); @@ -2138,6 +2373,7 @@ cache(function(data, match, sendBadge, request) { badgeData.text[1] = res; if (res === 'passed') { badgeData.colorscheme = 'brightgreen'; + badgeData.text[1] = 'passing'; } else if (res === 'failed' || res === 'error') { badgeData.colorscheme = 'red'; } else if (res === 'pending') { @@ -2166,8 +2402,8 @@ cache(function(data, match, sendBadge, request) { var slashesInRepo = 2; if (repoParts[0] === 'gp') { slashesInRepo = 1; } if ((repoParts.length - 1) > slashesInRepo) { - branch = repoParts[repoParts.length - 1]; - repo = repoParts.slice(0, -1).join('/'); + branch = repoParts.slice(slashesInRepo + 1).join('/'); + repo = repoParts.slice(0, slashesInRepo + 1).join('/'); } var apiUrl = 'https://scrutinizer-ci.com/api/repositories/' + repo; var badgeData = getBadgeData('code quality', data); @@ -2266,8 +2502,8 @@ cache(function(data, match, sendBadge, request) { } try { var nameMatch = buffer.match(/(devD|d)ependencies/)[0]; - var statusMatch = buffer.match(/'14'>(.+)<\/text>\n<\/g>/)[1]; - badgeData.text[0] = nameMatch; + var statusMatch = buffer.match(/'14'>(.+)<\/text>\s*<\/g>/)[1]; + badgeData.text[0] = data.label || nameMatch; badgeData.text[1] = statusMatch; if (statusMatch === 'up-to-date') { badgeData.colorscheme = 'brightgreen'; @@ -2306,7 +2542,9 @@ cache(function(data, match, sendBadge, request) { badgeData.text[1] = res; if (res === 'up to date') { badgeData.colorscheme = 'brightgreen'; - } else if (statusMatch === 'out of date') { + } else if (res === 'none') { + badgeData.colorscheme = 'green'; + } else if (res === 'out of date') { badgeData.colorscheme = 'yellow'; } else { badgeData.colorscheme = 'red'; @@ -2321,11 +2559,18 @@ cache(function(data, match, sendBadge, request) { })); // Codacy integration -camp.route(/^\/codacy\/(.+)\.(svg|png|gif|jpg|json)$/, +camp.route(/^\/codacy\/(?:grade\/)?(?!coverage\/)([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { var projectId = match[1]; // eg. e27821fb6289410b8f58338c7e0bc686 - var format = match[2]; - var url = 'https://www.codacy.com/project/badge/' + projectId; + var branch = match[2]; + var format = match[3]; + + queryParams = {}; + if (branch) { + queryParams.branch = branch; + } + var query = querystring.stringify(queryParams); + var url = 'https://www.codacy.com/project/badge/grade/' + projectId + '?' + query; var badgeData = getBadgeData('code quality', data); fetchFromSvg(request, url, function(err, res) { if (err != null) { @@ -2362,6 +2607,37 @@ cache(function(data, match, sendBadge, request) { }); })); +camp.route(/^\/codacy\/coverage\/(?!grade\/)([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var projectId = match[1]; // eg. e27821fb6289410b8f58338c7e0bc686 + var branch = match[2]; + var format = match[3]; + + queryParams = {}; + if (branch) { + queryParams.branch = branch; + } + var query = querystring.stringify(queryParams); + var url = 'https://www.codacy.com/project/badge/coverage/' + projectId + '?' + query; + var badgeData = getBadgeData('coverage', data); + fetchFromSvg(request, url, function(err, res) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + badgeData.text[1] = res; + badgeData.colorscheme = coveragePercentageColor(parseInt(res)); + sendBadge(format, badgeData); + + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Hackage version integration. camp.route(/^\/hackage\/v\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -2480,6 +2756,7 @@ cache(function(data, match, sendBadge, request) { }); })); +// CocoaPods metrics camp.route(/^\/cocoapods\/metrics\/doc-percent\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { var spec = match[1]; // eg, AFNetworking @@ -2506,6 +2783,82 @@ cache(function(data, match, sendBadge, request) { }); })); +// Cocoapods Downloads integration. +camp.route(/^\/cocoapods\/(dm|dw|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var info = match[1]; // One of these: "dm", "dw", "dt" + var spec = match[2]; // eg, AFNetworking + var format = match[3]; + var apiUrl = 'http://metrics.cocoapods.org/api/v1/pods/' + spec; + var badgeData = getBadgeData('downloads', data); + request(apiUrl, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + var downloads = 0; + switch (info.charAt(1)) { + case 'm': + downloads = data.stats.download_month; + badgeData.text[1] = metric(downloads) + '/month'; + break; + case 'w': + downloads = data.stats.download_week; + badgeData.text[1] = metric(downloads) + '/week'; + break; + case 't': + downloads = data.stats.download_total; + badgeData.text[1] = metric(downloads); + break; + } + badgeData.colorscheme = downloadCountColor(downloads); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// CocoaPods Apps Integration +camp.route(/^\/cocoapods\/(aw|at)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var info = match[1]; // One of these: "aw", "at" + var spec = match[2]; // eg, AFNetworking + var format = match[3]; + var apiUrl = 'http://metrics.cocoapods.org/api/v1/pods/' + spec; + var badgeData = getBadgeData('apps', data); + request(apiUrl, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + var apps = 0; + switch (info.charAt(1)) { + case 'w': + apps = data.stats.app_week; + badgeData.text[1] = metric(apps) + '/week'; + break; + case 't': + apps = data.stats.app_total; + badgeData.text[1] = metric(apps); + break; + } + badgeData.colorscheme = downloadCountColor(apps); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // GitHub tag integration. camp.route(/^\/github\/tag\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -2513,28 +2866,17 @@ cache(function(data, match, sendBadge, request) { var repo = match[2]; var format = match[3]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/tags'; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('tag', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); var versions = data.map(function(e) { return e.name; }); var tag = latestVersion(versions); @@ -2549,6 +2891,35 @@ cache(function(data, match, sendBadge, request) { }); })); +// GitHub contributors integration. +camp.route(/^\/github\/contributors(-anon)?\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var isAnon = match[1]; + var user = match[2]; // eg, qubyte/rubidium + var repo = match[3]; + var format = match[4]; + var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/contributors?page=1&per_page=1&anon=' + (0+isAnon); + var badgeData = getBadgeData('contributors', data); + if (badgeData.template === 'social') { + badgeData.logo = badgeData.logo || logos.github; + } + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + badgeData.text[1] = metric(data[0].contributions); + badgeData.colorscheme = 'blue'; + } catch(e) { + badgeData.text[1] = 'inaccessible'; + } + sendBadge(format, badgeData); + }); +})); + // GitHub release integration. camp.route(/^\/github\/release\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -2556,28 +2927,17 @@ cache(function(data, match, sendBadge, request) { var repo = match[2]; var format = match[3]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/releases/latest'; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('release', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); var version = data.tag_name; var prerelease = data.prerelease; @@ -2600,30 +2960,19 @@ cache(function(data, match, sendBadge, request) { var version = match[3]; // eg, 3.4.7 var format = match[4]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/compare/' + version + '...master'; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('commits since ' + version, data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); - badgeData.text[1] = data.ahead_by; + badgeData.text[1] = metric(data.ahead_by); badgeData.colorscheme = 'blue'; sendBadge(format, badgeData); } catch(e) { @@ -2639,7 +2988,7 @@ cache(function(data, match, sendBadge, request) { var user = match[1]; // eg, qubyte/rubidium var repo = match[2]; - var tag = match[3]; //null for all releases + var tag = match[3]; // eg, v0.190.0, latest, null if querying all releases var asset_name = match[4].toLowerCase(); // eg. total, atom-amd64.deb, atom.x86_64.rpm var format = match[5]; @@ -2655,27 +3004,16 @@ cache(function(data, match, sendBadge, request) { var release_path = tag !== 'latest' ? 'tags/' + tag : 'latest'; apiUrl = apiUrl + '/' + release_path; } - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('downloads', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; return sendBadge(format, badgeData); } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); var downloads = 0; @@ -2716,39 +3054,63 @@ cache(function(data, match, sendBadge, request) { })); // GitHub issues integration. -camp.route(/^\/github\/issues(-raw)?\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +camp.route(/^\/github\/issues(-pr)?(-closed)?(-raw)?\/([^\/]+)\/([^\/]+)\/?([^\/]+)?\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { - var isRaw = !!match[1]; - var user = match[2]; // eg, qubyte/rubidium - var repo = match[3]; - var format = match[4]; - var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; + var isPR = !!match[1]; + var isClosed = !!match[2]; + var isRaw = !!match[3]; + var user = match[4]; // eg, badges + var repo = match[5]; // eg, shields + var ghLabel = match[6]; // eg, website + var format = match[7]; + var apiUrl = 'https://api.github.com/'; + var query = {}; + var issuesApi = false; // Are we using the issues API instead of the repo one? + if (isPR) { + apiUrl += 'search/issues'; + query.q = 'is:pr is:' + (isClosed? 'closed': 'open') + + ' repo:' + user + '/' + repo; + } else { + apiUrl += 'repos/' + user + '/' + repo; + if (isClosed || ghLabel !== undefined) { + apiUrl += '/issues'; + if (isClosed) { query.state = 'closed'; } + if (ghLabel !== undefined) { query.labels = ghLabel; } + issuesApi = true; + } } - var badgeData = getBadgeData('issues', data); + + var closedText = isClosed? 'closed ': ''; + var targetText = isPR? 'pull requests': 'issues'; + var badgeData = getBadgeData(closedText + targetText, data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, query, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); - var issues = data.open_issues_count; - badgeData.text[1] = issues + (isRaw? '': ' open'); - badgeData.colorscheme = issues ? 'yellow' : 'brightgreen'; + var modifier = ''; + if (isPR) { + issues = data.total_count; + } else { + if (issuesApi) { + var issues = data.length; + if (res.headers['link'] && + res.headers['link'].indexOf('rel="last"') >= 0) { + modifier = '+'; + } + } else { + var issues = data.open_issues_count; + } + } + var rightText = isRaw? '': (isClosed? ' closed': ' open'); + badgeData.text[1] = metric(issues) + modifier + rightText; + badgeData.colorscheme = (issues > 0)? 'yellow': 'brightgreen'; sendBadge(format, badgeData); } catch(e) { badgeData.text[1] = 'invalid'; @@ -2764,12 +3126,6 @@ cache(function(data, match, sendBadge, request) { var repo = match[2]; var format = match[3]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('forks', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; @@ -2778,18 +3134,13 @@ cache(function(data, match, sendBadge, request) { 'https://github.com/' + user + '/' + repo + '/network', ]; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } var data = JSON.parse(buffer); var forks = data.forks_count; badgeData.text[1] = forks; @@ -2810,12 +3161,6 @@ cache(function(data, match, sendBadge, request) { var repo = match[2]; var format = match[3]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('stars', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; @@ -2824,19 +3169,14 @@ cache(function(data, match, sendBadge, request) { 'https://github.com/' + user + '/' + repo + '/stargazers', ]; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } - badgeData.text[1] = JSON.parse(buffer).stargazers_count; + badgeData.text[1] = metric(JSON.parse(buffer).stargazers_count); badgeData.colorscheme = null; badgeData.colorB = '#4183C4'; sendBadge(format, badgeData); @@ -2854,12 +3194,6 @@ cache(function(data, match, sendBadge, request) { var repo = match[2]; var format = match[3]; var apiUrl = 'https://api.github.com/repos/' + user + '/' + repo; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('watchers', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; @@ -2868,18 +3202,13 @@ cache(function(data, match, sendBadge, request) { 'https://github.com/' + user + '/' + repo + '/watchers', ]; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } badgeData.text[1] = JSON.parse(buffer).subscribers_count; badgeData.colorscheme = null; badgeData.colorB = '#4183C4'; @@ -2897,28 +3226,17 @@ cache(function(data, match, sendBadge, request) { var user = match[1]; // eg, qubyte var format = match[2]; var apiUrl = 'https://api.github.com/users/' + user; - // Using our OAuth App secret grants us 5000 req/hour - // instead of the standard 60 req/hour. - if (serverSecrets) { - apiUrl += '?client_id=' + serverSecrets.gh_client_id - + '&client_secret=' + serverSecrets.gh_client_secret; - } var badgeData = getBadgeData('followers', data); if (badgeData.template === 'social') { badgeData.logo = badgeData.logo || logos.github; } - // A special User-Agent is required: - // http://developer.github.com/v3/#user-agent-required - request(apiUrl, { headers: githubHeaders }, function(err, res, buffer) { + githubAuth.request(request, apiUrl, {}, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } badgeData.text[1] = JSON.parse(buffer).followers; badgeData.colorscheme = null; badgeData.colorB = '#4183C4'; @@ -2961,9 +3279,7 @@ cache(function(data, match, sendBadge, request) { return; } try { - if ((+res.headers['x-ratelimit-remaining']) === 0) { - return; // Hope for the best in the cache. - } else if (res.statusCode === 404) { + if (res.statusCode === 404) { badgeData.text[1] = 'repo not found'; sendBadge(format, badgeData); return; @@ -2984,6 +3300,36 @@ cache(function(data, match, sendBadge, request) { }); })); +// Bitbucket issues integration. +camp.route(/^\/bitbucket\/issues(-raw)?\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var isRaw = !!match[1]; + var user = match[2]; // eg, atlassian + var repo = match[3]; // eg, python-bitbucket + var format = match[4]; + var apiUrl = 'https://bitbucket.org/api/1.0/repositories/' + user + '/' + repo + + '/issues/?limit=0&status=new&status=open'; + + var badgeData = getBadgeData('issues', data); + request(apiUrl, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + var issues = data.count; + badgeData.text[1] = issues + (isRaw? '': ' open'); + badgeData.colorscheme = issues ? 'yellow' : 'brightgreen'; + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Chef cookbook integration. camp.route(/^\/cookbook\/v\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -3013,7 +3359,7 @@ cache(function(data, match, sendBadge, request) { }); })); -function mapNugetFeed(pattern, offset, getInfo) { +function mapNugetFeedv2(pattern, offset, getInfo) { var vRegex = new RegExp('^\\/' + pattern + '\\/v\\/(.*)\\.(svg|png|gif|jpg|json)$'); var vPreRegex = new RegExp('^\\/' + pattern + '\\/vpre\\/(.*)\\.(svg|png|gif|jpg|json)$'); var dtRegex = new RegExp('^\\/' + pattern + '\\/dt\\/(.*)\\.(svg|png|gif|jpg|json)$'); @@ -3130,7 +3476,7 @@ function mapNugetFeed(pattern, offset, getInfo) { } try { var downloads = data.DownloadCount; - badgeData.text[1] = metric(downloads) + ' total'; + badgeData.text[1] = metric(downloads); badgeData.colorscheme = downloadCountColor(downloads); sendBadge(format, badgeData); } catch(e) { @@ -3141,21 +3487,122 @@ function mapNugetFeed(pattern, offset, getInfo) { })); } -// NuGet and Chocolatey -mapNugetFeed('(nuget|chocolatey)', 1, function(match) { - var site = match[1]; +function mapNugetFeed(pattern, offset, getInfo) { + var vRegex = new RegExp('^\\/' + pattern + '\\/v\\/(.*)\\.(svg|png|gif|jpg|json)$'); + var vPreRegex = new RegExp('^\\/' + pattern + '\\/vpre\\/(.*)\\.(svg|png|gif|jpg|json)$'); + + function getNugetVersion(apiUrl, id, includePre, request, done) { + var reqUrl = apiUrl + '/flatcontainer/' + id.toLowerCase() + '/index.json'; + request(reqUrl, function(err, res, buffer) { + if (err != null) { + done(err); + return; + } + + try { + var data = JSON.parse(buffer); + var versions = data.versions; + if (!includePre) { + // Remove prerelease versions. + filteredVersions = versions.filter(function(version) { + return !/-/.test(version); + }); + if (filteredVersions.length > 0) { + versions = filteredVersions; + } + } + var lastVersion = versions[versions.length - 1]; + done(null, lastVersion); + } catch (e) { done(e); } + }); + } + + camp.route(vRegex, + cache(function(data, match, sendBadge, request) { + var info = getInfo(match); + var site = info.site; // eg, `Chocolatey`, or `YoloDev` + var repo = match[offset + 1]; // eg, `Nuget.Core`. + var format = match[offset + 2]; + var apiUrl = info.feed; + var badgeData = getBadgeData(site, data); + getNugetVersion(apiUrl, repo, false, request, function(err, version) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + badgeData.text[1] = 'v' + version; + if (version.indexOf('-') !== -1) { + badgeData.colorscheme = 'yellow'; + } else if (version[0] === '0') { + badgeData.colorscheme = 'orange'; + } else { + badgeData.colorscheme = 'blue'; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); + })); + + camp.route(vPreRegex, + cache(function(data, match, sendBadge, request) { + var info = getInfo(match); + var site = info.site; // eg, `Chocolatey`, or `YoloDev` + var repo = match[offset + 1]; // eg, `Nuget.Core`. + var format = match[offset + 2]; + var apiUrl = info.feed; + var badgeData = getBadgeData(site, data); + getNugetVersion(apiUrl, repo, true, request, function(err, version) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + badgeData.text[1] = 'v' + version; + if (version.indexOf('-') !== -1) { + badgeData.colorscheme = 'yellow'; + } else if (version[0] === '0') { + badgeData.colorscheme = 'orange'; + } else { + badgeData.colorscheme = 'blue'; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); + })); +} + +// Chocolatey +mapNugetFeedv2('chocolatey', 0, function(match) { return { - site: site, - feed: 'https://www.' + site + '.org/api/v2' + site: 'chocolatey', + feed: 'https://www.chocolatey.org/api/v2' + }; +}); + +// NuGet +mapNugetFeed('nuget', 0, function(match) { + return { + site: 'nuget', + feed: 'https://api.nuget.org/v3' }; }); // MyGet -mapNugetFeed('myget\\/(.*)', 1, function(match) { - var feed = match[1]; +mapNugetFeed('(.+\\.)?myget\\/(.*)', 2, function(match) { + var tenant = match[1] || 'www.'; // eg. dotnet + var feed = match[2]; return { site: feed, - feed: 'https://www.myget.org/F/' + feed + '/api/v2' + feed: 'https://' + tenant + 'myget.org/F/' + feed + '/api/v3' }; }); @@ -3191,7 +3638,7 @@ cache(function(data, match, sendBadge, request) { var total = json.downloads; badgeData.colorscheme = downloadCountColor(total); badgeData.text[0] = 'downloads'; - badgeData.text[1] = metric(total) + ' total'; + badgeData.text[1] = metric(total); } else if (info === 'e') { var endorsement = json.endorsement; if (endorsement === 'approved') { @@ -3276,6 +3723,13 @@ cache(function(data, match, sendBadge, request) { uri: scheme + '://' + host + '/job/' + job + '/api/json?tree=color' }; + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass + }; + } + var badgeData = getBadgeData('build', data); request(options, function(err, res, json) { if (err !== null) { @@ -3285,7 +3739,7 @@ cache(function(data, match, sendBadge, request) { } try { - if (json.color === 'blue') { + if (json.color === 'blue' || json.color === 'green') { badgeData.colorscheme = 'brightgreen'; badgeData.text[1] = 'passing'; } else if (json.color === 'red') { @@ -3323,6 +3777,13 @@ cache(function(data, match, sendBadge, request) { + '/lastBuild/api/json?tree=actions[failCount,skipCount,totalCount]' }; + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass + }; + } + var badgeData = getBadgeData('tests', data); request(options, function(err, res, json) { if (err !== null) { @@ -3359,6 +3820,59 @@ cache(function(data, match, sendBadge, request) { }); })); +// Jenkins coverage integration +camp.route(/^\/jenkins(-ci)?\/c\/(http(s)?)\/((?:[^\/]+)(?:\/.+?)?)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var scheme = match[2]; // http(s) + var host = match[4]; // jenkins.qa.ubuntu.com + var job = match[5]; // precise-desktop-amd64_default + var format = match[6]; + var options = { + json: true, + uri: scheme + '://' + host + '/job/' + job + + '/lastBuild/cobertura/api/json?tree=results[elements[name,denominator,numerator,ratio]]' + }; + + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass + }; + } + + var badgeData = getBadgeData('coverage', data); + request(options, function(err, res, json) { + if (err !== null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + + try { + var coverageObject = json.results.elements.filter(function (obj) { + return obj.name === 'Lines'; + })[0]; + if (coverageObject === undefined) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + var coverage = coverageObject.ratio; + if (+coverage !== +coverage) { + badgeData.text[1] = 'unknown'; + sendBadge(format, badgeData); + return; + } + badgeData.text[1] = coverage.toFixed(0) + '%'; + badgeData.colorscheme = coveragePercentageColor(coverage); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Ansible integration camp.route(/^\/ansible\/(role)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -3379,8 +3893,7 @@ cache(function(data, match, sendBadge, request) { } try { if (type === 'role') { - badgeData.text[1] = json.summary_fields.owner.username + - '.' + json.name; + badgeData.text[1] = json.namespace + '.' + json.name; badgeData.colorscheme = 'blue'; } else { badgeData.text[1] = 'unknown'; @@ -3400,7 +3913,7 @@ cache(function(data, match, sendBadge, request) { var branch = match[2]; var options = { method: 'GET', - uri: 'https://www.codeship.io/projects/' + projectId + '/status' + (branch != null ? '?branch=' + branch : '') + uri: 'https://codeship.com/projects/' + projectId + '/status' + (branch != null ? '?branch=' + branch : '') }; var badgeData = getBadgeData('build', data); request(options, function(err, res) { @@ -3420,7 +3933,7 @@ cache(function(data, match, sendBadge, request) { switch (statusMatch[1]) { case 'success': - badgeData.text[1] = 'passed'; + badgeData.text[1] = 'passing'; badgeData.colorscheme = 'brightgreen'; break; case 'projectnotfound': @@ -3449,6 +3962,53 @@ cache(function(data, match, sendBadge, request) { }); })); +// Magnum CI integration +camp.route(/^\/magnumci\/ci\/([^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var projectId = match[1]; // E.g. 96ffb83fa700f069024921b0702e76ff + var branch = match[2]; // E.g. master + var format = match[3]; + var options = { + method: 'GET', + uri: 'https://magnum-ci.com/status/' + projectId + '.png' + }; + if (branch != null) { + options.uri += '?branch=' + branch; + } + var badgeData = getBadgeData('build', data); + request(options, function(err, res) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var statusMatch = res.headers['content-disposition'] + .match(/filename="(.+)\.png"/); + if (!statusMatch) { + badgeData.text[1] = 'unknown'; + sendBadge(format, badgeData); + return; + } + + switch (statusMatch[1]) { + case 'pass': + badgeData.text[1] = 'passing'; + badgeData.colorscheme = 'brightgreen'; + break; + case 'fail': + badgeData.text[1] = 'failing'; + badgeData.colorscheme = 'red'; + break; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'not found'; + sendBadge(format, badgeData); + } + }); +})); + // Maven-Central artifact version integration // API documentation: http://search.maven.org/#api camp.route(/^\/maven-central\/v\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, @@ -3608,7 +4168,7 @@ cache(function(data, match, sendBadge, request) { } try { var total = JSON.parse(buffer).downloaded; - badgeData.text[1] = metric(total) + ' total'; + badgeData.text[1] = metric(total); if (total === 0) { badgeData.colorscheme = 'red'; } else if (total < 100) { @@ -3718,12 +4278,13 @@ cache(function(data, match, sendBadge, request) { })); // SourceForge integration. -camp.route(/^\/sourceforge\/([^\/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, +camp.route(/^\/sourceforge\/([^\/]+)\/([^/]*)\/?(.*).(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { var info = match[1]; // eg, 'dm' var project = match[2]; // eg, 'sevenzip`. - var format = match[3]; - var apiUrl = 'http://sourceforge.net/projects/' + project + '/files/stats/json'; + var folder = match[3]; + var format = match[4]; + var apiUrl = 'http://sourceforge.net/projects/' + project + '/files/' + folder + '/stats/json'; var badgeData = getBadgeData('sourceforge', data); var time_period, start_date, end_date; if (info.charAt(0) === 'd') { @@ -3835,7 +4396,7 @@ cache(function(data, match, sendBadge, request) { sendBadge(format, badgeData); return; } - badgeData.text[1] = metric(dls) + ' total'; + badgeData.text[1] = metric(dls); badgeData.colorscheme = 'green'; sendBadge(format, badgeData); }); @@ -4167,7 +4728,7 @@ cache(function(data, match, sendBadge, request) { user = 'library'; } var path = user + '/' + repo; - var url = 'https://registry.hub.docker.com/v2/repositories/' + path + '/stars/count/'; + var url = 'https://hub.docker.com/v2/repositories/' + path + '/stars/count/'; var badgeData = getBadgeData('docker stars', data); request(url, function(err, res, buffer) { if (err != null) { @@ -4198,7 +4759,7 @@ cache(function(data, match, sendBadge, request) { user = 'library'; } var path = user + '/' + repo; - var url = 'https://registry.hub.docker.com/v2/repositories/' + path; + var url = 'https://hub.docker.com/v2/repositories/' + path; var badgeData = getBadgeData('docker pulls', data); request(url, function(err, res, buffer) { if (err != null) { @@ -4220,22 +4781,19 @@ cache(function(data, match, sendBadge, request) { }); })); -// Twitter integration. -camp.route(/^\/twitter\/url\/([^\/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + +// Docker Hub automated integration. +camp.route(/^\/docker\/automated\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { - var scheme = match[1]; // eg, https - var path = match[2]; // eg, shields.io + var user = match[1]; // eg, jrottenberg + var repo = match[2]; // eg, ffmpeg var format = match[3]; - var page = encodeURIComponent(scheme + '://' + path); - var url = 'http://cdn.api.twitter.com/1/urls/count.json?url=' + page; - var badgeData = getBadgeData('tweet', data); - if (badgeData.template === 'social') { - badgeData.logo = badgeData.logo || logos.twitter; - badgeData.links = [ - 'https://twitter.com/intent/tweet?text=Wow:&url=' + page, - 'https://twitter.com/search?q=' + page, - ]; + if (user === '_') { + user = 'library'; } + var path = user + '/' + repo; + var url = 'https://registry.hub.docker.com/v2/repositories/' + path; + var badgeData = getBadgeData('docker build', data); request(url, function(err, res, buffer) { if (err != null) { badgeData.text[1] = 'inaccessible'; @@ -4244,8 +4802,15 @@ cache(function(data, match, sendBadge, request) { } try { var data = JSON.parse(buffer); - badgeData.text[1] = metric(data.count); - badgeData.colorscheme = '55ACEE'; + var is_automated = data.is_automated; + if (is_automated) { + badgeData.text[1] = 'automated'; + badgeData.colorscheme = 'blue'; + } else { + badgeData.text[1] = 'manual'; + badgeData.colorscheme = 'yellow'; + } + badgeData.colorB = '#008bb8'; sendBadge(format, badgeData); } catch(e) { badgeData.text[1] = 'invalid'; @@ -4254,6 +4819,68 @@ cache(function(data, match, sendBadge, request) { }); })); +// Twitter integration. +camp.route(/^\/twitter\/url\/([^\/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var scheme = match[1]; // eg, https + var path = match[2]; // eg, shields.io + var format = match[3]; + var page = encodeURIComponent(scheme + '://' + path); + // The URL API died: #568. + //var url = 'http://cdn.api.twitter.com/1/urls/count.json?url=' + page; + var badgeData = getBadgeData('tweet', data); + if (badgeData.template === 'social') { + badgeData.logo = badgeData.logo || logos.twitter; + badgeData.links = [ + 'https://twitter.com/intent/tweet?text=Wow:&url=' + page, + 'https://twitter.com/search?q=' + page, + ]; + } + badgeData.text[1] = ''; + badgeData.colorscheme = null; + badgeData.colorB = '#55ACEE'; + sendBadge(format, badgeData); +})); + +// Twitter follow badge. +camp.route(/^\/twitter\/follow\/@?([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var user = match[1]; // eg, shields_io + var format = match[2]; + var options = { + url: 'http://cdn.syndication.twimg.com/widgets/followbutton/info.json?screen_names=' + user + }; + var badgeData = getBadgeData('Follow @' + user, data); + + badgeData.colorscheme = null; + badgeData.colorB = '#55ACEE'; + if (badgeData.template === 'social') { + badgeData.logo = badgeData.logo || logos.twitter; + } + badgeData.links = [ + 'https://twitter.com/intent/follow?screen_name=' + user, + 'https://twitter.com/' + user + '/followers' + ]; + badgeData.text[1] = ''; + request(options, function(err, res, buffer) { + if (err != null) { + sendBadge(format, badgeData); + return; + } + try { + // The data is formatted as an array. + var data = JSON.parse(buffer)[0]; + // data.followers_count could be zero… don't just check if falsey. + if (data !== undefined && data.followers_count != null){ + badgeData.text[1] = metric(data.followers_count); + } + } catch(e) { + console.error(e); + } + sendBadge(format, badgeData); + }); +})); + // Snap CI build integration. // https://snap-ci.com/snap-ci/snap-deploy/branch/master/build_image camp.route(/^\/snap(-ci?)\/([^\/]+\/[^\/]+)(?:\/(.+))\.(svg|png|gif|jpg|json)$/, @@ -4274,6 +4901,7 @@ cache(function(data, match, sendBadge, request) { badgeData.text[1] = res.toLowerCase(); if (res === 'Passed') { badgeData.colorscheme = 'brightgreen'; + badgeData.text[1] = 'passing'; } else if (res === 'Failed') { badgeData.colorscheme = 'red'; } @@ -4286,6 +4914,38 @@ cache(function(data, match, sendBadge, request) { }); })); +// Visual Studio Team Services build integration. +camp.route(/^\/vso\/build\/([^\/]+)\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var name = match[1]; // User name + var project = match[2]; // Project ID, e.g. 953a34b9-5966-4923-a48a-c41874cfb5f5 + var build = match[3]; // Build definition ID, e.g. 1 + var format = match[4]; + var url = 'https://' + name + '.visualstudio.com/DefaultCollection/_apis/public/build/definitions/' + project + '/' + build + '/badge'; + var badgeData = getBadgeData('build', data); + fetchFromSvg(request, url, function(err, res) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + badgeData.text[1] = res.toLowerCase(); + if (res === 'succeeded') { + badgeData.colorscheme = 'brightgreen'; + badgeData.text[1] = 'passing'; + } else if (res === 'failed') { + badgeData.colorscheme = 'red'; + badgeData.text[1] = 'failing'; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // ImageLayers.io integration. camp.route(/^\/imagelayers\/(image\-size|layers)\/([^\/]+)\/([^\/]+)\/([^\/]*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -4342,11 +5002,423 @@ cache(function(data, match, sendBadge, request) { badgeData.colorscheme = 'brightgreen'; if (darkBackgroundTemplates.some(function(t) { return t === badgeData.template; })) { badgeData.logo = badgeData.logo || logos['gitter-white']; - badgeData.logoWidth = 7; + badgeData.logoWidth = 9; } sendBadge(format, badgeData); })); +// homebrew integration +camp.route(/^\/homebrew\/v\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var pkg = match[1]; // eg. cake + var format = match[2]; + var apiUrl = 'http://braumeister.org/formula/' + pkg + '/version'; + + var badgeData = getBadgeData('homebrew', data); + request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + } + try { + var data = JSON.parse(buffer); + var version = data.stable; + + var vdata = versionColor(version); + badgeData.text[1] = vdata.version; + badgeData.colorscheme = vdata.color; + + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// StackExchange integration. +camp.route(/^\/stackexchange\/([^\/]+)\/([^\/])\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var site = match[1]; // eg, stackoverflow + var info = match[2]; // either `r` + var item = match[3]; // eg, 232250 + var format = match[4]; + var path; + if (info === 'r') { + path = 'users/' + item; + } else if (info === 't') { + path = 'tags/' + item + '/info'; + } + var options = { + method: 'GET', + uri: 'https://api.stackexchange.com/2.2/' + path + '?site=' + site, + gzip: true + } + var badgeData = getBadgeData(site, data); + request(options, function (err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(badgeData, format); + return; + } + try { + var data = JSON.parse(buffer.toString()); + + // IP rate limiting + if (data.error_name === 'throttle_violation') { + return; // Hope for the best in the cache. + } + + if (info === 'r') { + var reputation = data.items[0].reputation; + badgeData.text[0] = site + ' reputation'; + badgeData.text[1] = metric(reputation); + badgeData.colorscheme = floorCountColor(1000, 10000, 20000); + } else if (info === 't') { + var count = data.items[0].count; + badgeData.text[0] = site + ' ' + item + ' questions'; + badgeData.text[1] = metric(count); + badgeData.colorscheme = floorCountColor(1000, 10000, 20000); + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + })} +)); + +// beerpay.io integration. +// e.g. JSON response: https://beerpay.io/api/v1/beerpay/projects/beerpay.io +// e.g. SVG badge: https://beerpay.io/beerpay/beerpay.io/badge.svg?style=flat-square +camp.route(/^\/beerpay\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var user = match[1]; // eg, beerpay + var project = match[2]; // eg, beerpay.io + var format = match[3]; + + var apiUrl = 'https://beerpay.io/api/v1/' + user + '/projects/' + project; + var badgeData = getBadgeData('beerpay', data); + + request(apiUrl, function (err, res, buffer) { + if (err) { + badgeData.text[1] = 'inaccessible'; + sendBadge(badgeData, format); + return; + } + + try { + var data = JSON.parse(buffer); + badgeData.text[1] = '$' + (data.total_amount || 0); + badgeData.colorscheme = 'red'; + sendBadge(format, badgeData); + } catch (e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// Maintenance integration. +camp.route(/^\/maintenance\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var status = match[1]; // eg, yes + var year = +match[2]; // eg, 2016 + var format = match[3]; + var badgeData = getBadgeData('maintained?', data); + try { + var now = new Date(); + var cy = now.getUTCFullYear(); // current year. + var m = now.getUTCMonth(); // month. + if (cy <= year) { + badgeData.text[1] = status; + badgeData.colorscheme = 'brightgreen'; + } else if ((cy === year + 1) && (m < 3)) { + badgeData.text[1] = 'stale (as of ' + cy + ')'; + } else { + badgeData.text[1] = 'no!'; + badgeData.colorscheme = 'red'; + } + sendBadge(format, badgeData); + } catch(e) { + console.error(e.stack); + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } +})); + +// bitHound integration +camp.route(/^\/bithound\/(code\/|dependencies\/|devDependencies\/)?(.+?)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var type = match[1].slice(0, -1); + var userRepo = match[2]; // eg, `github/rexxars/sse-channel`. + var format = match[3]; + var apiUrl = 'https://www.bithound.io/api/' + userRepo + '/badge/' + type; + var badgeData = getBadgeData(type === 'devDependencies' ? 'dev dependencies' : type, data); + + request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) { + try { + var data = JSON.parse(buffer); + badgeData.text[1] = data.label; + badgeData.logo = logos['bithound']; + badgeData.logoWidth = 15; + badgeData.colorscheme = null; + badgeData.colorB = '#' + data.color; + sendBadge(format, badgeData); + + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// Waffle.io integration +camp.route(/^\/waffle\/label\/([^\/]+)\/([^\/]+)\/?([^\/]+)?\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var user = match[1]; // eg, evancohen + var repo = match[2]; // eg, smart-mirror + var ghLabel = match[3] || 'ready'; // eg, in%20progress + var format = match[4]; + var apiUrl = 'https://api.waffle.io/' + user + '/' + repo + '/cards'; + var badgeData = getBadgeData('issues', data); + + request(apiUrl, function(err, res, buffer) { + try { + var cards = JSON.parse(buffer); + if (cards.length === 0) { + badgeData.text[1] = 'absent'; + sendBadge(format, badgeData); + return; + } + var count = 0; + var color; + for (var i = 0; i < cards.length; i++) { + var cardMetadata = cards[i].githubMetadata; + if (cardMetadata.labels && cardMetadata.labels.length > 0) { + for (var j = 0; j < cardMetadata.labels.length; j++) { + var label = cardMetadata.labels[j]; + if (label.name === ghLabel) { + count++; + color = label.color; + } + } + } + } + badgeData.text[0] = data.label || ghLabel; + badgeData.text[1] = '' + count; + badgeData.colorscheme = null; + badgeData.colorB = '#' + (color || '78bdf2'); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// Arch user repository (AUR) integration. +camp.route(/^\/aur\/(version|votes|license)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var info = match[1]; + var pkg = match[2]; + var format = match[3]; + var apiUrl = 'https://aur.archlinux.org/rpc.php?type=info&arg=' + pkg; + var badgeData = getBadgeData('AUR', data); + request(apiUrl, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer).results; + if (info === 'version') { + var vdata = versionColor(data.Version); + badgeData.text[1] = vdata.version; + if (data.OutOfDate === null) { + badgeData.colorscheme = 'blue'; + } else { + badgeData.colorscheme = 'orange'; + } + } else if (info === 'votes') { + var votes = data.NumVotes; + badgeData.text[0] = "votes"; + badgeData.text[1] = votes; + badgeData.colorscheme = floorCountColor(votes, 2, 20, 60); + } else if (info === 'license') { + var license = data.License; + badgeData.text[0] = "license"; + badgeData.text[1] = license; + badgeData.colorscheme = 'blue'; + } + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + +// Chrome web store integration +camp.route(/^\/chrome-web-store\/(v|d|price|rating|rating-count)\/(.*)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var type = match[1]; + var storeId = match[2]; // eg, nimelepbpejjlbmoobocpfnjhihnpked + var format = match[3]; + var badgeData = getBadgeData('chrome web store', data); + var url = 'https://chrome.google.com/webstore/detail/' + storeId + '?hl=en&gl=US'; + var chromeWebStore = require('chrome-web-store-item-property'); + request(url, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + chromeWebStore.convert(buffer) + .then(function (value) { + if (type === 'v') { + var vdata = versionColor(value.version); + badgeData.text[1] = vdata.version; + badgeData.colorscheme = vdata.color; + } else if (type === 'd') { + var downloads = value.interactionCount.UserDownloads; + badgeData.text[0] = 'downloads'; + badgeData.text[1] = metric(downloads) + ' total'; + badgeData.colorscheme = downloadCountColor(downloads); + } else if (type === 'price') { + badgeData.text[1] = value.price; + badgeData.colorscheme = 'brightgreen'; + } else if (type === 'rating') { + var rating = Math.round(value.ratingValue * 100) / 100; + badgeData.text[0] = 'rating'; + badgeData.text[1] = rating; + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); + } else if (type === 'rating-count') { + var ratingCount = value.ratingCount; + badgeData.text[0] = 'rating count'; + badgeData.text[1] = metric(ratingCount) + ' total'; + badgeData.colorscheme = floorCountColor(ratingCount, 5, 50, 500); + } + sendBadge(format, badgeData); + }).catch(function (err) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + }); + }); +})); + +// Test if a webpage is online +camp.route(/^\/website(-(([^-]|--)*?)-(([^-]|--)*)(-(([^-]|--)+)-(([^-]|--)+))?)?\/(.+)\/(.+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var onlineMessage = escapeFormat(match[2] != null ? match[2] : "online"); + var offlineMessage = escapeFormat(match[4] != null ? match[4] : "offline"); + var onlineColor = escapeFormat(match[7] != null ? match[7] : "brightgreen"); + var offlineColor = escapeFormat(match[9] != null ? match[9] : "red"); + var userProtocol = match[11]; + var userURI = match[12]; + var format = match[13]; + var withProtocolURI = userProtocol + "://" + userURI; + var options = { + method: 'HEAD', + uri: withProtocolURI, + }; + var badgeData = getBadgeData('website', data); + badgeData.colorscheme = undefined; + request(options, function(err, res) { + try { + // We consider all HTTP status codes below 310 as success. + if (err != null || res.statusCode >= 310) { + badgeData.text[1] = offlineMessage; + if (sixHex(offlineColor)) { + badgeData.colorB = '#' + offlineColor; + } else { + badgeData.colorscheme = offlineColor; + } + sendBadge(format, badgeData); + return; + } else { + badgeData.text[1] = onlineMessage; + if (sixHex(onlineColor)) { + badgeData.colorB = '#' + onlineColor; + } else { + badgeData.colorscheme = onlineColor; + } + sendBadge(format, badgeData); + return; + } + } catch(e) { + badge({text: ['error', 'bad badge'], colorscheme: 'red'}, + makeSend(format, ask.res, end)); + } + }); +})); + +// Issue Stats integration. +camp.route(/^\/issuestats\/([^\/]+)(\/long)?\/([^\/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var type = match[1]; // e.g. `i` for Issue or `p` for PR + var longForm = !!match[2]; + var host = match[3]; // e.g. `github` + var userRepo = match[4]; // e.g. `ruby/rails` + var format = match[5]; + + var badgeData = getBadgeData('Issue Stats', data); + + // Maps type name from URL to JSON property name prefix for badge data + var typeToPropPrefix = { + i: 'issue', + p: 'pr' + }; + var typePropPrefix = typeToPropPrefix[type]; + if (typePropPrefix === undefined) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + return; + } + + var url = 'http://issuestats.com/' + host + '/' + userRepo; + var qs = {format: 'json'}; + if (!longForm) { + qs.concise = true; + } + var options = { + method: 'GET', + url: url, + qs: qs, + gzip: true, + json: true + }; + request(options, function(err, res, json) { + if (err != null || res.statusCode >= 500) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + return; + } + + if (res.statusCode >= 400 || !json || typeof json !== 'object') { + badgeData.text[1] = 'not found'; + sendBadge(format, badgeData); + return; + } + + try { + var label = json[typePropPrefix + '_badge_preamble']; + var value = json[typePropPrefix + '_badge_words']; + var color = json[typePropPrefix + '_badge_color']; + + if (label != null) badgeData.text[0] = label; + badgeData.text[1] = value || 'invalid'; + if (color != null) badgeData.colorscheme = color; + + sendBadge(format, badgeData); + } catch (e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Libraries.io integration. camp.route(/^\/librariesio\/(github|release)\/([\w\-\_]+\/[\w\-\_]+)\/?([\w\-\_\.]+)?\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { @@ -4406,8 +5478,6 @@ cache(function(data, match, sendBadge, request) { }); })); - - // Any badge. camp.route(/^\/(:|badge\/)(([^-]|--)*?)-(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/, function(data, match, end, ask) { @@ -4423,13 +5493,9 @@ function(data, match, end, ask) { incrMonthlyAnalytics(analytics.rawFlatSquareMonthly); } - if (ask.req.headers['cache-control']) { - ask.res.setHeader('Cache-Control', ask.req.headers['cache-control']); - } else { - // Cache management - the badge is constant. - var cacheDuration = (3600*24*1)|0; // 1 day. - ask.res.setHeader('Cache-Control', 'public, max-age=' + cacheDuration); - } + // Cache management - the badge is constant. + var cacheDuration = (3600*24*1)|0; // 1 day. + ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration); if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) { ask.res.statusCode = 304; ask.res.end(); // not modified. @@ -4459,6 +5525,21 @@ function(data, match, end, ask) { } }); +// Production cache debugging. +var bitFlip = false; +camp.route(/^\/flip\.svg$/, function(data, match, end, ask) { + var cacheSecs = 60; + ask.res.setHeader('Cache-Control', 'max-age=' + cacheSecs); + var reqTime = new Date(); + var date = (new Date(+reqTime + cacheSecs * 1000)).toGMTString(); + ask.res.setHeader('Expires', date); + var badgeData = getBadgeData('flip', data); + bitFlip = !bitFlip; + badgeData.text[1] = bitFlip? 'on': 'off'; + badgeData.colorscheme = bitFlip? 'brightgreen': 'red'; + badge(badgeData, makeSend('svg', ask.res, end)); +}); + // Any badge, old version. camp.route(/^\/([^\/]+)\/(.+).png$/, function(data, match, end, ask) { @@ -4468,7 +5549,7 @@ function(data, match, end, ask) { // Cache management - the badge is constant. var cacheDuration = (3600*24*1)|0; // 1 day. - ask.res.setHeader('Cache-Control', 'public, max-age=' + cacheDuration); + ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration); if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) { ask.res.statusCode = 304; ask.res.end(); // not modified. @@ -4513,12 +5594,20 @@ function getLabel(label, data) { } // data (URL query) can include `label`, `style`, `logo`, `logoWidth`, `link`. +// It can also include `maxAge`. function getBadgeData(defaultLabel, data) { var label = getLabel(defaultLabel, data); var template = data.style || 'default'; if (data.style && validTemplates.indexOf(data.style) > -1) { template = data.style; }; + if (!(Object(data.link) instanceof Array)) { + if (data.link === undefined) { + data.link = []; + } else { + data.link = [data.link]; + } + } if (data.logo !== undefined && !/^data:/.test(data.logo)) { data.logo = 'data:' + data.logo; @@ -4595,11 +5684,6 @@ function regularUpdate(url, interval, scraper, cb) { }); } -var githubHeaders = { - 'User-Agent': 'Shields.io', - 'Accept': 'application/vnd.github.v3+json' -}; - // Given a number, string with appropriate unit in the metric system, SI. // Note: numbers beyond the peta- cannot be represented as integers in JS. var metricPrefix = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; @@ -4608,7 +5692,7 @@ var metricPower = metricPrefix function metric(n) { for (var i = metricPrefix.length - 1; i >= 0; i--) { var limit = metricPower[i]; - if (n > limit) { + if (n >= limit) { n = Math.round(n / limit); return ''+n + metricPrefix[i]; } @@ -4674,27 +5758,52 @@ function versionColor(version) { } } +// Take string versions. +// -1 if v1 < v2, 1 if v1 > v2, 0 otherwise. +function compareDottedVersion(v1, v2) { + var parts1 = /([0-9\.]+)(.*)$/.exec(v1); + var parts2 = /([0-9\.]+)(.*)$/.exec(v2); + if (parts1 != null && parts2 != null) { + var numbers1 = parts1[1]; + var numbers2 = parts2[1]; + var distinguisher1 = parts1[2]; + var distinguisher2 = parts2[2]; + var numlist1 = numbers1.split('.').map(function(e) { return +e; }); + var numlist2 = numbers2.split('.').map(function(e) { return +e; }); + var cmp = listCompare(numlist1, numlist2); + if (cmp !== 0) { return cmp; } + else { return distinguisher1 < distinguisher2? -1: + distinguisher1 > distinguisher2? 1: 0; } + } + return v1 < v2? -1: v1 > v2? 1: 0; +} + +// Take a list of string versions. +// Return the latest, or undefined, if there are none. +function latestDottedVersion(versions) { + var len = versions.length; + if (len === 0) { return; } + var version = versions[0]; + for (var i = 1; i < len; i++) { + if (compareDottedVersion(version, versions[i]) < 0) { + version = versions[i]; + } + } + return version; +} + // Given a list of versions (as strings), return the latest version. +// Return undefined if no version could be found. function latestVersion(versions) { var version = ''; var origVersions = versions; versions = versions.filter(function(version) { return (/^v?[0-9]/).test(version); }); - semver_versions = versions.map(function(version) { - var matches = /^(v?[0-9]+)(\.[0-9]+)?(-.*)?$/.exec(version); - if (matches) { - version = matches[1] + (matches[2] ? matches[2] : '.0') + '.0' + - (matches[3] ? matches[3] : ''); - } - return version; - }); try { - version = semver.maxSatisfying(semver_versions, ''); - version = versions[semver_versions.indexOf(version)]; + version = semver.maxSatisfying(versions, ''); } catch(e) { - versions = versions.sort(); - version = versions[versions.length - 1]; + version = latestDottedVersion(versions); } if (version === undefined) { origVersions = origVersions.sort(); @@ -4810,20 +5919,15 @@ function phpNumberedVersionData(version) { } function listCompare(a, b) { - for (var i = 0; i < a.length; i++) { + var alen = a.length, blen = b.length; + for (var i = 0; i < alen; i++) { if (a[i] < b[i]) { return -1; } else if (a[i] > b[i]) { return 1; } } - if (a.length < b.length) { - return -1; - } else if (a.length > b.length) { - return 1; - } else { - return 0; - } + return alen - blen; } // Return a negative value if v1 < v2, @@ -4887,3 +5991,42 @@ function phpStableVersion(version) { // normal or patch return (versionData.modifier === 3) || (versionData.modifier === 4); } + +// This searches the serverSecrets for a twitter consumer key +// and secret, and exchanges them for a bearer token to use for all requests. +function fetchTwitterToken() { + if(serverSecrets.twitter_consumer_key && serverSecrets.twitter_consumer_secret){ + // fetch a bearer token good for this app session + // construct this bearer request with a base64 encoding of key:secret + // docs for this are here: https://dev.twitter.com/oauth/application-only + var twitter_bearer_credentials = escape(serverSecrets.twitter_consumer_key) + ':' + escape(serverSecrets.twitter_consumer_secret); + var options = { + url: 'https://api.twitter.com/oauth2/token', + headers: { + // is this the best way to base 64 encode a string? + Authorization: 'Basic '+(new Buffer(twitter_bearer_credentials)).toString('base64'), + 'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + form: 'grant_type=client_credentials', + method: 'POST' + }; + console.log('Fetching twitter bearer token...'); + request(options,function(err,res,buffer){ + if(err){ + console.error('Error fetching twitter bearer token, error: ', err); + return; + } + try{ + var data = JSON.parse(buffer); + if(data.token_type === 'bearer'){ + serverSecrets.twitter_bearer_token = data.access_token; + console.log('Fetched twitter bearer token'); + return; + } + console.error('Error fetching twitter bearer token, data: %j', data); + } catch(e) { + console.error('Error fetching twitter bearer token, buffer: %s, ', buffer, e); + } + }); + } +} diff --git a/suggest.js b/suggest.js index 35a5c25..8ad4fc3 100644 --- a/suggest.js +++ b/suggest.js @@ -1,6 +1,5 @@ var nodeUrl = require('url'); var request = require('request'); -var Promise = require('promise'); var serverSecrets; try { // Everything that cannot be checked in but is useful server-side @@ -185,11 +184,14 @@ var githubLicense = function(user, repo) { // Key phrases for common licenses var licensePhrases = { - 'Apache 1.1': 'apache (software)? license,? (version)? 1\\.1', - 'Apache 2': 'apache (software)? license,? (version)? 2', + 'Apache 1.1': 'apache (software )?license,? (version)? 1\\.1', + 'Apache 2': 'apache (software )?license,? (version)? 2', 'Original BSD': 'all advertising materials mentioning features or use of this software must display the following acknowledgement', 'New BSD': 'may be used to endorse or promote products derived from this software without specific prior written permission', 'BSD': 'redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met', + 'AGPLv1': 'affero general public license,? version 1', + 'AGPLv3': 'affero general public license,? version 3', + 'AGPL': 'affero general public license', 'GPLv2': 'gnu general public license,? version 2', 'GPLv3': 'gnu general public license,? version 3', 'GPL': 'gnu general public license', @@ -205,9 +207,6 @@ var licensePhrases = { 'Eclipse': 'eclipse public license', 'Artistic': 'artistic license', 'zlib': 'the origin of this software must not be misrepresented', - 'AGPLv1': 'affero general public license,? version 1', - 'AGPLv3': 'affero general public license,? version 3', - 'AGPL': 'affero general public license', 'ISC': 'permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted', 'CC0': 'cc0', 'Unlicense': 'this is free and unencumbered software released into the public domain', diff --git a/svg-to-img.js b/svg-to-img.js index 662bbb8..b777570 100644 --- a/svg-to-img.js +++ b/svg-to-img.js @@ -1,7 +1,7 @@ var fs = require('fs'); var os = require('os'); var path = require('path'); -var phantom = require('phantomjs'); +var phantom = require('phantomjs-prebuilt'); var LruCache = require('./lru-cache.js'); var childProcess = require('child_process'); var phantomScript = path.join(__dirname, 'phantomjs-svg2png.js'); diff --git a/templates/flat-square-template.svg b/templates/flat-square-template.svg index dc10083..87d37ee 100644 --- a/templates/flat-square-template.svg +++ b/templates/flat-square-template.svg @@ -1,10 +1,13 @@ - + - {{=it.escapeXml(it.text[0])}} + {{?it.logo}} + + {{?}} + {{=it.escapeXml(it.text[0])}} {{=it.escapeXml(it.text[1])}} diff --git a/templates/social-template.svg b/templates/social-template.svg index 442187f..386236e 100644 --- a/templates/social-template.svg +++ b/templates/social-template.svg @@ -14,9 +14,11 @@ + {{?(it.text[1] && it.text[1].length)}} + {{?}} {{?it.logo}} @@ -24,10 +26,12 @@ {{=it.escapeXml(it.capitalize(it.text[0]))}} {{=it.escapeXml(it.capitalize(it.text[0]))}} + {{?(it.text[1] && it.text[1].length)}} {{=it.escapeXml(it.text[1])}} {{=it.escapeXml(it.text[1])}} + {{?}} diff --git a/test.js b/test.js deleted file mode 100644 index 5aec33e..0000000 --- a/test.js +++ /dev/null @@ -1,151 +0,0 @@ -var ass = require('ass').enable(); -var should = require('should'); - -var http = require('http'); -var cproc = require('child_process'); -var fs = require('fs'); - -describe('the CLI', function() { - it('should provide a help message', function(done) { - var child = cproc.spawn('node', ['ass-stubs/cli-test.js']); - var buffer = ''; - child.stdout.on('data', function(chunk) { - buffer += ''+chunk; - }); - child.stdout.on('end', function() { - buffer.should.startWith('Usage'); - done(); - }); - }); - it('should produce default badges', function(done) { - var child = cproc.spawn('node', - ['ass-stubs/cli-test.js', 'cactus', 'grown']); - child.stdout.on('data', function(chunk) { - var buffer = ''+chunk; - buffer.should.startWith('= 0 && !isDone) { done(); isDone = true; } - }); - server.stderr.on('data', function(data) { console.log(''+data); }); - }); - - it('should produce colorscheme badges', function(done) { - http.get(url + ':fruit-apple-green.svg', - function(res) { - var buffer = ''; - res.on('data', function(chunk) { buffer += ''+chunk; }); - res.on('end', function() { - buffer.should.startWith('= 0 && !isDone) { done(); isDone = true; } + }); + server.stderr.on('data', function(data) { console.log(''+data); }); + }], + ['should produce colorscheme badges', function(done, assert) { + http.get(url + ':fruit-apple-green.svg', + function(res) { + var buffer = ''; + res.on('data', function(chunk) { buffer += ''+chunk; }); + res.on('end', function() { + assert(buffer.startsWith(' tbody > tr > td > img { cursor: pointer; } +table.badge > tbody > tr > th, +table.badge > tbody > tr > td > img, +table.badge > tbody > tr > td > code { cursor: pointer; }
@@ -104,6 +108,14 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable
https://img.shields.io/codeship/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/master.svg + Magnum CI: + + https://img.shields.io/magnumci/ci/96ffb83fa700f069024921b0702e76ff.svg + + Magnum CI: + + https://img.shields.io/magnumci/ci/96ffb83fa700f069024921b0702e76ff/new-meta.svg + CircleCI: https://img.shields.io/circleci/project/BrightFlair/PHP.Gt.svg @@ -116,13 +128,17 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable https://img.shields.io/circleci/token/YOURTOKEN/project/BrightFlair/PHP.Gt/master.svg + Visual Studio Team services: + + https://img.shields.io/vso/build/larsbrinkhoff/953a34b9-5966-4923-a48a-c41874cfb5f5/1.svg + Shippable: - - https://img.shields.io/shippable/54d119db5ab6cc13528ab183.svg + + https://img.shields.io/shippable/5444c5ecb904a4b21567b0ff.svg Shippable branch: - - https://img.shields.io/shippable/54d119db5ab6cc13528ab183/master.svg + + https://img.shields.io/shippable/5444c5ecb904a4b21567b0ff/master.svg Snap CI branch: @@ -136,6 +152,10 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable https://img.shields.io/jenkins/t/https/jenkins.qa.ubuntu.com/precise-desktop-amd64_default.svg + Jenkins coverage: + + https://img.shields.io/jenkins/c/https/jenkins.qa.ubuntu.com/address-book-service-utopic-i386-ci.svg + Coveralls: https://img.shields.io/coveralls/jekyll/jekyll.svg @@ -211,19 +231,19 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable

Downloads

- + - + - + - + @@ -251,14 +271,6 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable - - - - - - - - @@ -327,6 +339,10 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + @@ -351,6 +367,42 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Github All Releases:
Github All Releases: https://img.shields.io/github/downloads/atom/atom/total.svg
Github Releases:
Github Releases: https://img.shields.io/github/downloads/atom/atom/latest/total.svg
Github Releases (by Release):
Github Releases (by Release): https://img.shields.io/github/downloads/atom/atom/v0.190.0/total.svg
Github Releases (by Asset):
Github Releases (by Asset): https://img.shields.io/github/downloads/atom/atom/latest/atom-amd64.deb.svg
https://img.shields.io/gem/dt/rails.svg
NuGet: https://img.shields.io/nuget/dt/Microsoft.AspNet.Mvc.svg
MyGet: https://img.shields.io/myget/yolodev/dt/FSharpSupport.svg
Chocolatey: https://img.shields.io/chocolatey/dt/scriptcs.svg https://img.shields.io/sourceforge/dt/sevenzip.svg
SourceForge: https://img.shields.io/sourceforge/dt/arianne/stendhal.svg
apm: https://img.shields.io/apm/dm/vim-mode.svg https://img.shields.io/dub/dt/vibe-d/0.7.23.svg
Package Control: https://img.shields.io/packagecontrol/dm/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dw/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dd/Package%20Control.svg
Package Control: https://img.shields.io/packagecontrol/dt/Package%20Control.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/d/nimelepbpejjlbmoobocpfnjhihnpked.svg
Website: https://img.shields.io/website-up-down-green-red/http/shields.io.svg
CocoaPods: https://img.shields.io/cocoapods/dt/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/dm/AFNetworking.svg
CocoaPods: https://img.shields.io/cocoapods/dw/AFNetworking.svg

Version

@@ -358,6 +410,10 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + @@ -410,15 +466,15 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable - + - + - + @@ -431,16 +487,20 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable - - + + - - + + - - + + + + + + @@ -478,23 +538,35 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + + + + + + + + +
https://img.shields.io/npm/v/npm.svg
npm (scoped): https://img.shields.io/npm/v/@cycle/core.svg
node: https://img.shields.io/node/v/gh-badges.svg https://img.shields.io/hexpm/v/plug.svg
GitHub tag:
GitHub tag: https://img.shields.io/github/tag/strongloop/express.svg
GitHub release:
GitHub release: https://img.shields.io/github/release/qubyte/rubidium.svg
GitHub commits:
GitHub commits: https://img.shields.io/github/commits-since/SubtitleEdit/subtitleedit/3.4.7.svg
https://img.shields.io/nuget/v/Nuget.Core.svg
NuGet Pre Release: https://img.shields.io/nuget/vpre/Nuget.Core.svghttps://img.shields.io/nuget/vpre/Microsoft.AspNet.Mvc.svg
MyGet: https://img.shields.io/myget/yolodev/v/FSharpSupport.svghttps://img.shields.io/myget/mongodb/v/MongoDB.Driver.Core.svg
MyGet Pre Release: https://img.shields.io/myget/yolodev/vpre/FSharpSupport.svghttps://img.shields.io/myget/yolodev/vpre/YoloDev.Dnx.FSharp.svg
MyGet tenant: https://img.shields.io/dotnet.myget/dotnet-coreclr/v/Microsoft.DotNet.CoreCLR.svg
Chocolatey: https://img.shields.io/dub/v/vibe-d.svg
AUR: https://img.shields.io/aur/version/yaourt.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/v/nimelepbpejjlbmoobocpfnjhihnpked.svg
homebrew: https://img.shields.io/homebrew/v/cake.svg

Social

- + - + - + - + @@ -502,18 +574,30 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + +
GitHub forks:
GitHub forks: https://img.shields.io/github/forks/badges/shields.svg?style=social&label=Fork
GitHub stars:
GitHub stars: https://img.shields.io/github/stars/badges/shields.svg?style=social&label=Star
GitHub watchers:
GitHub watchers: https://img.shields.io/github/watchers/badges/shields.svg?style=social&label=Watch
GitHub followers:
GitHub followers: https://img.shields.io/github/followers/espadrine.svg?style=social&label=Follow
https://img.shields.io/twitter/url/http/shields.io.svg?style=social
Twitter Follow: https://img.shields.io/twitter/follow/shields_io.svg?style=social&label=Follow

Miscellaneous

- - - + + + + + + + + + + + @@ -522,6 +606,14 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + + + + + @@ -602,25 +694,77 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + @@ -670,6 +814,10 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + @@ -682,6 +830,37 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Gratipay: https://img.shields.io/gratipay/JSFiddle.svg
Gratipay User: https://img.shields.io/gratipay/user/dougwilson.svg
Gratipay Team: https://img.shields.io/gratipay/team/shields.svg
Bountysource: https://img.shields.io/bountysource/team/mozilla-core/activity.svg
Beerpay: https://img.shields.io/beerpay/hashdog/scrapfy-chrome-extension.svg
Code Climate: https://img.shields.io/codeclimate/github/kabisaict/flow.svg https://img.shields.io/codeclimate/coverage/github/triAGENS/ashikawa-core.svg
Code Climate: https://img.shields.io/codeclimate/issues/github/me-and/mdf.svg
bitHound: https://img.shields.io/bithound/code/github/rexxars/sse-channel.svg
Gemnasium: https://img.shields.io/gemnasium/mathiasbynens/he.svg https://img.shields.io/wheelmap/a/2323004600.svg
GitHub issues:
GitHub issues: https://img.shields.io/github/issues/badges/shields.svg
https://img.shields.io/github/issues-raw/badges/shields.svg
GitHub license:
GitHub closed issues: https://img.shields.io/github/issues-closed/badges/shields.svg
https://img.shields.io/github/issues-closed-raw/badges/shields.svg
label:https://img.shields.io/github/issues-raw/badges/shields/website.svg
GitHub pull requests: https://img.shields.io/github/issues-pr/cdnjs/cdnjs.svg
https://img.shields.io/github/issues-pr-raw/cdnjs/cdnjs.svg
GitHub closed pull requests: https://img.shields.io/github/issues-pr-closed/cdnjs/cdnjs.svg
https://img.shields.io/github/issues-pr-closed-raw/cdnjs/cdnjs.svg
GitHub contributors: https://img.shields.io/github/contributors/cdnjs/cdnjs.svg
license: https://img.shields.io/github/license/mashape/apistatus.svg
Bitbucket issues: https://img.shields.io/bitbucket/issues/atlassian/python-bitbucket.svg
https://img.shields.io/bitbucket/issues-raw/atlassian/python-bitbucket.svg
WordPress rating: https://img.shields.io/wordpress/plugin/r/akismet.svg
Codacy: https://img.shields.io/codacy/e27821fb6289410b8f58338c7e0bc686.svg
Codacy grade: https://img.shields.io/codacy/grade/e27821fb6289410b8f58338c7e0bc686.svg
Codacy branch grade: https://img.shields.io/codacy/grade/e27821fb6289410b8f58338c7e0bc686/master.svg
Codacy coverage: https://img.shields.io/codacy/coverage/c44df2d9c89a4809896914fd1a40bedd.svg
Codacy branch coverage: https://img.shields.io/codacy/coverage/c44df2d9c89a4809896914fd1a40bedd/master.svg
Libscore: https://img.shields.io/docker/pulls/mashape/kong.svg
Docker Automated build https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg
ImageLayers Size: https://img.shields.io/imagelayers/image-size/_/ubuntu/latest.svg https://img.shields.io/gitter/room/nwjs/nw.js.svg
JIRA issue: https://img.shields.io/jira/issue/https/issues.apache.org/jira/KAFKA-2896.svg
Maintenance: https://img.shields.io/maintenance/yes/2016.svg
AUR: https://img.shields.io/aur/license/yaourt.svg
Waffle.io: https://img.shields.io/waffle/label/evancohen/smart-mirror/in%20progress.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/price/nimelepbpejjlbmoobocpfnjhihnpked.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/rating/nimelepbpejjlbmoobocpfnjhihnpked.svg
Chrome Web Store: https://img.shields.io/chrome-web-store/rating-count/nimelepbpejjlbmoobocpfnjhihnpked.svg
AUR: https://img.shields.io/aur/votes/yaourt.svg

Longer Miscellaneous

@@ -702,6 +881,22 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable https://img.shields.io/david/peer/webcomponents/generator-element.svg + bitHound: + + https://img.shields.io/bithound/dependencies/github/rexxars/sse-channel.svg + + bitHound: + + https://img.shields.io/bithound/devDependencies/github/rexxars/sse-channel.svg + + CocoaPods: + + https://img.shields.io/cocoapods/at/AFNetworking.svg + + CocoaPods: + + https://img.shields.io/cocoapods/aw/AFNetworking.svg + CocoaPods: https://img.shields.io/cocoapods/p/AFNetworking.svg @@ -714,6 +909,29 @@ Pixel-perfect   Retina-ready   Fast   Consistent   Hackable https://img.shields.io/ansible/role/3078.svg + StackExchange: + + https://img.shields.io/stackexchange/tex/r/951.svg + + StackExchange: + + https://img.shields.io/stackexchange/stackoverflow/t/augeas.svg + + Issue Stats: + + https://img.shields.io/issuestats/i/github/strongloop/express.svg + + (long form): +   + https://img.shields.io/issuestats/i/long/github/strongloop/express.svg + + Issue Stats: + + https://img.shields.io/issuestats/p/github/strongloop/express.svg + (long form): +   + https://img.shields.io/issuestats/p/long/github/strongloop/express.svg + Libraries.io for releases: https://img.shields.io/librariesio/release/hex/phoenix/1.0.3.svg @@ -810,6 +1028,8 @@ Here are a few other parameters you can use: ?link=http://abc.xyz&link=http://shields.io Specify what clicking on the left/right of a badge should do (esp. for social badge style) + ?maxAge=3600 + Set the HTTP cache lifetime in secs

@@ -824,11 +1044,9 @@ Tell your favorite badge service to use it!
And tell us, we might be able to bring it to you anyway!

- - +Follow @shields_io Donate to us! -

@@ -837,127 +1055,11 @@ And tell us, we might be able to bring it to you anyway! is where the current server got started.

-

Contributors

- - - espadrine - - - mathiasbynens - - - olivierlacan - - - nathany - - - whit537 - - - kura - - - maxogden - - - seanlinsley - - - alrra - - - jublo - - - g105b - - - Daniel15 - -
- - sebmck - - - avanderhoorn - - - stefanjudis - - - jmalloc - - - ezzatron - - - brettcannon - - - CodeBlock - - - Mikulas - - - segiddins - - - SonicHedgehog - - - akashivskyy - - - akashivskyy - -
- - rmasters - - - cainus - - - jbowes - - - rafalchmiel - - - fjcaetano - - - hughsk - - - qubyte - - - khellang - - - nitram509 - - - raphink - - - montanaflynn - - - PeterDaveHello - -
- - sagiegurari -

:wq

-
+

@@ -972,9 +1074,83 @@ is where the current server got started.

reStructuredText +

+ + @@ -1082,14 +1258,6 @@ function selectNode(e) { }; copyMarkdown.addEventListener('click', selectNode); copyreStructuredText.addEventListener('click', selectNode); -function autoselectCode() { - var codes = document.querySelectorAll('table.badge code'); - for (var i = 0; i < codes.length; i++) { - codes[i].addEventListener('click', selectNode); - } -} - -document.addEventListener('DOMContentLoaded', autoselectCode); // Markup copier dialog // @@ -1098,10 +1266,14 @@ function markupDialogInit() { var trs = document.querySelectorAll('table.badge tr'); for (var i = 0; i < trs.length; i++) { var tr = trs[i]; + var title = tr.querySelector('th'); var target = tr.querySelector('img'); - if (target) { - target.addEventListener('click', makeMarkupDialogListener(tr)); - } + var code = tr.querySelector('code'); + // Markup dialog listener. + var mdl = makeMarkupDialogListener(tr); + if (title != null) { title.addEventListener('click', mdl); } + if (target != null) { target.addEventListener('click', mdl); } + if (code != null) { code.addEventListener('click', mdl); } } } function makeMarkupDialogListener(tr) { @@ -1119,8 +1291,18 @@ function markupDialog(tr) { var trimg = tr.querySelector('img').src; var th = tr.firstElementChild; var link = th.dataset.link? th.dataset.link: ''; + // Remove the ?maxAge parameter from the query string. + trimg.replace(/[\?&]maxAge=\d+$|maxAge=\d+&/, ''); copyForm.img.value = trimg; copyForm.url.value = link; + // Insert documentation. + var doc = th.dataset.doc? th.dataset.doc: ''; + var docelt = document.getElementById(doc); + if (docelt != null) { + copyDoc.innerHTML = '

Documentation

' + docelt.innerHTML; + } else { + copyDoc.innerHTML = ''; + } // Set up the input listeners. copyForm.url.removeEventListener('input', copyFormUrlEventListener); copyForm.img.removeEventListener('input', copyFormUrlEventListener);