diff --git a/lib/color-formatters.js b/lib/color-formatters.js new file mode 100644 index 0000000000000..cf291e0be3528 --- /dev/null +++ b/lib/color-formatters.js @@ -0,0 +1,45 @@ +/** + * Commonly-used functions for determining the colour to use for a badge, + * including colours based off download count, version number, etc. + */ +'use strict'; + +function versionFormatter(version) { + var first = version[0]; + if (first === 'v') { + first = version[1]; + } else if (/^[0-9]/.test(version)) { + version = 'v' + version; + } + if (first === '0' || (version.indexOf('-') !== -1)) { + return { version: version, color: 'orange' }; + } else { + return { version: version, color: 'blue' }; + } +} +exports.version = versionFormatter; + +function downloadCount(downloads) { + return floorCount(downloads, 10, 100, 1000); +} +exports.downloadCount = downloadCount; + +function coveragePercentage(percentage) { + return floorCount(percentage, 80, 90, 100); +} +exports.coveragePercentage = coveragePercentage; + +function floorCount(value, yellow, yellowgreen, green) { + if (value === 0) { + return 'red'; + } else if (value < yellow) { + return 'yellow'; + } else if (value < yellowgreen) { + return 'yellowgreen'; + } else if (value < green) { + return 'green'; + } else { + return 'brightgreen'; + } +} +exports.floorCount = floorCount; diff --git a/lib/php-version.js b/lib/php-version.js new file mode 100644 index 0000000000000..fa501612253dc --- /dev/null +++ b/lib/php-version.js @@ -0,0 +1,181 @@ +/** + * Utilities relating to PHP version numbers. This compares version numbers + * using the algorithm followed by Composer (see + * https://getcomposer.org/doc/04-schema.md#version). + */ +'use strict'; + +const {listCompare} = require('./version.js'); + +// Return a negative value if v1 < v2, +// zero if v1 = v2, +// a positive value otherwise. +// +// See https://getcomposer.org/doc/04-schema.md#version +// and https://github.com/badges/shields/issues/319#issuecomment-74411045 +function compare(v1, v2) { + // Omit the starting `v`. + var rawv1 = omitv(v1); + var rawv2 = omitv(v2); + try { + var v1data = numberedVersionData(rawv1); + var v2data = numberedVersionData(rawv2); + } catch(e) { + return asciiVersionCompare(rawv1, rawv2); + } + + // Compare the numbered part (eg, 1.0.0 < 2.0.0). + var numbersCompare = listCompare(v1data.numbers, v2data.numbers); + if (numbersCompare !== 0) { + return numbersCompare; + } + + // Compare the modifiers (eg, alpha < beta). + if (v1data.modifier < v2data.modifier) { + return -1; + } else if (v1data.modifier > v2data.modifier) { + return 1; + } + + // Compare the modifier counts (eg, alpha1 < alpha3). + if (v1data.modifierCount < v2data.modifierCount) { + return -1; + } else if (v1data.modifierCount > v2data.modifierCount) { + return 1; + } + + return 0; +} +exports.compare = compare; + +function latest(versions) { + var latest = versions[0]; + for (var i = 1; i < versions.length; i++) { + if (compare(latest, versions[i]) < 0) { + latest = versions[i]; + } + } + return latest; +} +exports.latest = latest; + +// Whether a version is stable. +function isStable(version) { + var rawVersion = omitv(version); + try { + var versionData = numberedVersionData(rawVersion); + } catch(e) { + return false; + } + // normal or patch + return (versionData.modifier === 3) || (versionData.modifier === 4); +} +exports.isStable = isStable; + +// === Private helper functions === + +// Remove the starting v in a string. +function omitv(version) { + if (version.charCodeAt(0) === 118) { // v + return version.slice(1); + } else { + return version; + } +} + +// Return a negative value if v1 < v2, +// zero if v1 = v2, a positive value otherwise. +function asciiVersionCompare(v1, v2) { + if (v1 < v2) { + return -1; + } else if (v1 > v2) { + return 1; + } else { + return 0; + } +} + +// Take a version without the starting v. +// eg, '1.0.x-beta' +// Return { numbers: [1,0,something big], modifier: 2, modifierCount: 1 } +function numberedVersionData(version) { + // A version has a numbered part and a modifier part + // (eg, 1.0.0-patch, 2.0.x-dev). + var parts = version.split('-'); + var numbered = parts[0]; + + // Aliases that get caught here. + if (numbered === 'dev') { + return { + numbers: parts[1], + modifier: 5, + modifierCount: 1, + }; + } + + var modifierLevel = 3; + var modifierLevelCount = 0; + + if (parts.length > 1) { + var modifier = parts[parts.length - 1]; + var firstLetter = modifier.charCodeAt(0); + var modifierLevelCountString; + + // Modifiers: alpha < beta < RC < normal < patch < dev + if (firstLetter === 97) { // a + modifierLevel = 0; + if (/^alpha/.test(modifier)) { + modifierLevelCountString = + (modifier.slice(5)); + } else { + modifierLevelCountString = + (modifier.slice(1)); + } + } else if (firstLetter === 98) { // b + modifierLevel = 1; + if (/^beta/.test(modifier)) { + modifierLevelCountString = + (modifier.slice(4)); + } else { + modifierLevelCountString = + (modifier.slice(1)); + } + } else if (firstLetter === 82) { // R + modifierLevel = 2; + modifierLevelCountString = + (modifier.slice(2)); + } else if (firstLetter === 112) { // p + modifierLevel = 4; + if (/^patch/.test(modifier)) { + modifierLevelCountString = + (modifier.slice(5)); + } else { + modifierLevelCountString = + (modifier.slice(1)); + } + } else if (firstLetter === 100) { // d + modifierLevel = 5; + if (/^dev/.test(modifier)) { + modifierLevelCountString = + (modifier.slice(3)); + } else { + modifierLevelCountString = + (modifier.slice(1)); + } + } + + // If we got the empty string, it defaults to a modifier count of 1. + if (!modifierLevelCountString) { + modifierLevelCount = 1; + } else { + modifierLevelCount = + modifierLevelCountString; + } + } + + // Try to convert to a list of numbers. + var toNum = function(s) { + var n = +s; + if (n !== n) { // If n is NaN… + n = 0xffffffff; + } + return n; + }; + var numberList = numbered.split('.').map(toNum); + + return { + numbers: numberList, + modifier: modifierLevel, + modifierCount: modifierLevelCount, + }; +} diff --git a/lib/text-formatters.js b/lib/text-formatters.js new file mode 100644 index 0000000000000..24a0bafd87c11 --- /dev/null +++ b/lib/text-formatters.js @@ -0,0 +1,47 @@ +/** + * Commonly-used functions for formatting text in badge labels. Includes + * ordinal numbers, currency codes, star ratings, etc. + */ +'use strict'; + +function starRating(rating) { + var stars = ''; + while (stars.length < rating) { stars += '★'; } + while (stars.length < 5) { stars += '☆'; } + return stars; +} +exports.starRating = starRating; + +// Convert ISO 4217 code to unicode string. +function currencyFromCode(code) { + return ({ + CNY: '¥', + EUR: '€', + GBP: '₤', + USD: '$', + })[code] || code; +} +exports.currencyFromCode = currencyFromCode; + +function ordinalNumber(n) { + var s=["ᵗʰ","ˢᵗ","ⁿᵈ","ʳᵈ"], v=n%100; + return n+(s[(v-20)%10]||s[v]||s[0]); +} +exports.ordinalNumber = ordinalNumber; + +// 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']; +var metricPower = metricPrefix + .map(function(a, i) { return Math.pow(1000, i + 1); }); +function metric(n) { + for (var i = metricPrefix.length - 1; i >= 0; i--) { + var limit = metricPower[i]; + if (n >= limit) { + n = Math.round(n / limit); + return ''+n + metricPrefix[i]; + } + } + return ''+n; +} +exports.metric = metric; diff --git a/lib/version.js b/lib/version.js new file mode 100644 index 0000000000000..09b7d5500b922 --- /dev/null +++ b/lib/version.js @@ -0,0 +1,80 @@ +/** + * Utilities relating to generating badges relating to version numbers. Includes + * comparing versions to determine the latest, and determining the color to use + * for the badge based on whether the version is a stable release. + * + * For utilities specific to PHP version ranges, see php-version.js. + */ +'use strict'; + +const semver = require('semver'); + +// Given a list of versions (as strings), return the latest version. +// Return undefined if no version could be found. +function latest(versions) { + var version = ''; + var origVersions = versions; + versions = versions.filter(function(version) { + return (/^v?[0-9]/).test(version); + }); + try { + version = semver.maxSatisfying(versions, ''); + } catch(e) { + version = latestDottedVersion(versions); + } + if (version === undefined) { + origVersions = origVersions.sort(); + version = origVersions[origVersions.length - 1]; + } + return version; +} +exports.latest = latest; + +function listCompare(a, b) { + 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; + } + } + return alen - blen; +} +exports.listCompare = listCompare; + +// === Private helper functions === + +// 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; +} + +// 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; +} diff --git a/server.js b/server.js index 88af91f8c4365..fefac6595e77a 100644 --- a/server.js +++ b/server.js @@ -38,6 +38,25 @@ if (serverSecrets && serverSecrets.gh_client_id) { githubAuth.setRoutes(camp); } +const {latest: latestVersion} = require('./lib/version.js'); +const { + compare: phpVersionCompare, + latest: phpLatestVersion, + isStable: phpStableVersion, +} = require('./lib/php-version.js'); +const { + currencyFromCode, + metric, + ordinalNumber, + starRating, +} = require('./lib/text-formatters.js'); +const { + coveragePercentage: coveragePercentageColor, + downloadCount: downloadCountColor, + floorCount: floorCountColor, + version: versionColor, +} = require('./lib/color-formatters.js'); + var semver = require('semver'); var serverStartTime = new Date((new Date()).toGMTString()); @@ -6276,23 +6295,6 @@ function regularUpdate(url, interval, scraper, cb) { }); } -// 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']; -var metricPower = metricPrefix - .map(function(a, i) { return Math.pow(1000, i + 1); }); -function metric(n) { - for (var i = metricPrefix.length - 1; i >= 0; i--) { - var limit = metricPower[i]; - if (n >= limit) { - n = Math.round(n / limit); - return ''+n + metricPrefix[i]; - } - } - return ''+n; -} - - // Get data from a svg-style badge. // cb: function(err, string) function fetchFromSvg(request, url, cb) { @@ -6308,295 +6310,3 @@ function fetchFromSvg(request, url, cb) { } }); } - -function ordinalNumber(n) { - var s=["ᵗʰ","ˢᵗ","ⁿᵈ","ʳᵈ"], v=n%100; - return n+(s[(v-20)%10]||s[v]||s[0]); -} - -// Convert ISO 4217 code to unicode string. -function currencyFromCode(code) { - return ({ - CNY: '¥', - EUR: '€', - GBP: '₤', - USD: '$', - })[code] || code; -} - -function starRating(rating) { - var stars = ''; - while (stars.length < rating) { stars += '★'; } - while (stars.length < 5) { stars += '☆'; } - return stars; -} - -function coveragePercentageColor(percentage) { - return floorCountColor(percentage, 80, 90, 100); -} - -function downloadCountColor(downloads) { - return floorCountColor(downloads, 10, 100, 1000); -} - -function floorCountColor(value, yellow, yellowgreen, green) { - if (value === 0) { - return 'red'; - } else if (value < yellow) { - return 'yellow'; - } else if (value < yellowgreen) { - return 'yellowgreen'; - } else if (value < green) { - return 'green'; - } else { - return 'brightgreen'; - } -} - -function versionColor(version) { - var first = version[0]; - if (first === 'v') { - first = version[1]; - } else if (/^[0-9]/.test(version)) { - version = 'v' + version; - } - if (first === '0' || (version.indexOf('-') !== -1)) { - return { version: version, color: 'orange' }; - } else { - return { version: version, color: 'blue' }; - } -} - -// 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); - }); - try { - version = semver.maxSatisfying(versions, ''); - } catch(e) { - version = latestDottedVersion(versions); - } - if (version === undefined) { - origVersions = origVersions.sort(); - version = origVersions[origVersions.length - 1]; - } - return version; -} - -// Return a negative value if v1 < v2, -// zero if v1 = v2, a positive value otherwise. -function asciiVersionCompare(v1, v2) { - if (v1 < v2) { - return -1; - } else if (v1 > v2) { - return 1; - } else { - return 0; - } -} - -// Remove the starting v in a string. -function omitv(version) { - if (version.charCodeAt(0) === 118) { // v - return version.slice(1); - } else { - return version; - } -} - -// Take a version without the starting v. -// eg, '1.0.x-beta' -// Return { numbers: [1,0,something big], modifier: 2, modifierCount: 1 } -function phpNumberedVersionData(version) { - // A version has a numbered part and a modifier part - // (eg, 1.0.0-patch, 2.0.x-dev). - var parts = version.split('-'); - var numbered = parts[0]; - - // Aliases that get caught here. - if (numbered === 'dev') { - return { - numbers: parts[1], - modifier: 5, - modifierCount: 1, - }; - } - - var modifierLevel = 3; - var modifierLevelCount = 0; - - if (parts.length > 1) { - var modifier = parts[parts.length - 1]; - var firstLetter = modifier.charCodeAt(0); - var modifierLevelCountString; - - // Modifiers: alpha < beta < RC < normal < patch < dev - if (firstLetter === 97) { // a - modifierLevel = 0; - if (/^alpha/.test(modifier)) { - modifierLevelCountString = + (modifier.slice(5)); - } else { - modifierLevelCountString = + (modifier.slice(1)); - } - } else if (firstLetter === 98) { // b - modifierLevel = 1; - if (/^beta/.test(modifier)) { - modifierLevelCountString = + (modifier.slice(4)); - } else { - modifierLevelCountString = + (modifier.slice(1)); - } - } else if (firstLetter === 82) { // R - modifierLevel = 2; - modifierLevelCountString = + (modifier.slice(2)); - } else if (firstLetter === 112) { // p - modifierLevel = 4; - if (/^patch/.test(modifier)) { - modifierLevelCountString = + (modifier.slice(5)); - } else { - modifierLevelCountString = + (modifier.slice(1)); - } - } else if (firstLetter === 100) { // d - modifierLevel = 5; - if (/^dev/.test(modifier)) { - modifierLevelCountString = + (modifier.slice(3)); - } else { - modifierLevelCountString = + (modifier.slice(1)); - } - } - - // If we got the empty string, it defaults to a modifier count of 1. - if (!modifierLevelCountString) { - modifierLevelCount = 1; - } else { - modifierLevelCount = + modifierLevelCountString; - } - } - - // Try to convert to a list of numbers. - var toNum = function(s) { - var n = +s; - if (n !== n) { // If n is NaN… - n = 0xffffffff; - } - return n; - }; - var numberList = numbered.split('.').map(toNum); - - return { - numbers: numberList, - modifier: modifierLevel, - modifierCount: modifierLevelCount, - }; -} - -function listCompare(a, b) { - 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; - } - } - return alen - blen; -} - -// Return a negative value if v1 < v2, -// zero if v1 = v2, -// a positive value otherwise. -// -// See https://getcomposer.org/doc/04-schema.md#version -// and https://github.com/badges/shields/issues/319#issuecomment-74411045 -function phpVersionCompare(v1, v2) { - // Omit the starting `v`. - var rawv1 = omitv(v1); - var rawv2 = omitv(v2); - try { - var v1data = phpNumberedVersionData(rawv1); - var v2data = phpNumberedVersionData(rawv2); - } catch(e) { - return asciiVersionCompare(rawv1, rawv2); - } - - // Compare the numbered part (eg, 1.0.0 < 2.0.0). - var numbersCompare = listCompare(v1data.numbers, v2data.numbers); - if (numbersCompare !== 0) { - return numbersCompare; - } - - // Compare the modifiers (eg, alpha < beta). - if (v1data.modifier < v2data.modifier) { - return -1; - } else if (v1data.modifier > v2data.modifier) { - return 1; - } - - // Compare the modifier counts (eg, alpha1 < alpha3). - if (v1data.modifierCount < v2data.modifierCount) { - return -1; - } else if (v1data.modifierCount > v2data.modifierCount) { - return 1; - } - - return 0; -} - -function phpLatestVersion(versions) { - var latest = versions[0]; - for (var i = 1; i < versions.length; i++) { - if (phpVersionCompare(latest, versions[i]) < 0) { - latest = versions[i]; - } - } - return latest; -} - -// Whether a version is stable. -function phpStableVersion(version) { - var rawVersion = omitv(version); - try { - var versionData = phpNumberedVersionData(rawVersion); - } catch(e) { - return false; - } - // normal or patch - return (versionData.modifier === 3) || (versionData.modifier === 4); -}