Support named logos and omit logos by default (#1092)

- Except for social badges, omit logos by default (#983)
- Omit the logo from a social badge using the query string: `?logo=`
  (#983)
- Opt in to named logos using the query string: `?logo=appveyor`
- Provide custom logo data as before: `?logo=data:image/png;base64,...`
- Rewrite badge data functions, with unit tests

Unit tests are covering the new code very well, though the underlying
functionality (logos) is untested.

Close #983
This commit is contained in:
Paul Melnikow
2017-09-28 10:47:39 -04:00
parent 450d861ee5
commit 47ba81a007
3 changed files with 206 additions and 78 deletions

99
lib/badge-data.js Normal file
View File

@@ -0,0 +1,99 @@
const logos = require('./load-logos')();
function toArray(val) {
if (val === undefined) {
return [];
} else if (Object(val) instanceof Array) {
return val;
} else {
return [val];
}
}
function isDataUri(s) {
return s !== undefined && /^(data:)([^;]+);([^,]+),(.+)$/.test(s);
}
function hasPrefix(s, prefix) {
return s !== undefined && s.slice(0, prefix.length) === prefix;
}
function prependPrefix(s, prefix) {
if (s === undefined) {
return undefined;
} else if (hasPrefix(s, prefix)) {
return s;
} else {
return prefix + s;
}
}
function isValidStyle(style) {
const validStyles = ['default', 'plastic', 'flat', 'flat-square', 'social'];
return style ? validStyles.indexOf(style) >= 0 : false;
}
function isSixHex (s){
return s !== undefined && /^[0-9a-fA-F]{6}$/.test(s);
}
function makeColor(color) {
if (isSixHex(color)) {
return '#' + color;
} else {
return color;
}
}
function makeLabel(defaultLabel, overrides) {
return overrides.label || defaultLabel;
}
function makeLogo(defaultNamedLogo, overrides) {
const maybeDataUri = prependPrefix(overrides.logo, 'data:');
const maybeNamedLogo = overrides.logo === undefined ? defaultNamedLogo : overrides.logo;
if (isDataUri(maybeDataUri)) {
return maybeDataUri;
} else {
return logos[maybeNamedLogo];
}
}
// Generate the initial badge data. Pass the URL query parameters, which
// override the default label.
//
// The following parameters are supported:
//
// - label
// - style
// - logo
// - logoWidth
// - link
// - colorA
// - colorB
// - maxAge
//
// Note: maxAge is handled by cache(), not this function.
function makeBadgeData(defaultLabel, overrides) {
return {
text: [makeLabel(defaultLabel, overrides), 'n/a'],
colorscheme: 'lightgrey',
template: isValidStyle(overrides.style) ? overrides.style : 'default',
logo: makeLogo(undefined, overrides),
logoWidth: +overrides.logoWidth,
links: toArray(overrides.link),
colorA: makeColor(overrides.colorA),
colorB: makeColor(overrides.colorB),
};
}
module.exports = {
hasPrefix,
isDataUri,
isValidStyle,
isSixHex,
makeLabel,
makeLogo,
makeBadgeData,
};

84
lib/badge-data.spec.js Normal file
View File

@@ -0,0 +1,84 @@
const assert = require('assert');
const {
isDataUri,
hasPrefix,
isValidStyle,
isSixHex,
makeLabel,
makeLogo,
makeBadgeData,
} = require('./badge-data');
describe('Badge data helpers', function() {
it('should detect prefixes', function() {
assert.equal(hasPrefix('data:image/svg+xml;base64,PHN2ZyB4bWxu', 'data:'), true);
assert.equal(hasPrefix('data:foobar', 'data:'), true);
assert.equal(hasPrefix('foobar', 'data:'), false);
});
it('should detect valid image data URIs', function() {
assert.equal(isDataUri('data:image/svg+xml;base64,PHN2ZyB4bWxu'), true);
assert.equal(isDataUri('data:foobar'), false);
assert.equal(isDataUri('foobar'), false);
});
it('should detect valid styles', function() {
assert.equal(isValidStyle('flat'), true);
assert.equal(isValidStyle('flattery'), false);
assert.equal(isValidStyle(''), false);
assert.equal(isValidStyle(undefined), false);
});
it('should detect valid six-hex strings', function() {
assert.equal(isSixHex('f00bae'), true);
assert.equal(isSixHex('f00bar'), false);
assert.equal(isSixHex(''), false);
assert.equal(isSixHex(undefined), false);
});
it('should make labels', function() {
assert.equal(makeLabel('my badge', {}), 'my badge');
assert.equal(makeLabel('my badge', { label: 'no, my badge' }), 'no, my badge');
});
it('should make logos', function() {
assert.equal(
makeLogo('gratipay', { logo: 'image/svg+xml;base64,PHN2ZyB4bWxu' }),
'data:image/svg+xml;base64,PHN2ZyB4bWxu');
assert.equal(
makeLogo('gratipay', { logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu' }),
'data:image/svg+xml;base64,PHN2ZyB4bWxu');
assert.equal(
makeLogo('gratipay', { logo: '' }),
undefined);
assert.equal(
makeLogo(undefined, {}),
undefined);
assert.ok(isDataUri(makeLogo('gratipay', {})));
});
it('should make badge data', function() {
const overrides = {
label: 'no, my badge',
style: 'flat-square',
logo: 'image/svg+xml;base64,PHN2ZyB4bWxu',
logoWidth: '25',
link: 'https://example.com/',
colorA: 'blue',
colorB: 'f00bae',
};
const expected = {
text: ['no, my badge', 'n/a'],
colorscheme: 'lightgrey',
template: 'flat-square',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
logoWidth: 25,
links: ['https://example.com/'],
colorA: 'blue',
colorB: '#f00bae',
};
assert.deepEqual(makeBadgeData('my badge', overrides), expected);
});
});

101
server.js
View File

@@ -26,7 +26,6 @@ var log = require('./lib/log.js');
var LruCache = require('./lib/lru-cache.js');
var badge = require('./lib/badge.js');
var svg2img = require('./lib/svg-to-img.js');
var loadLogos = require('./lib/load-logos.js');
var githubAuth = require('./lib/github-auth.js');
var querystring = require('querystring');
var prettyBytes = require('pretty-bytes');
@@ -65,14 +64,17 @@ const {
incrMonthlyAnalytics,
getAnalytics
} = require('./lib/analytics');
const {
isValidStyle,
isSixHex: sixHex,
makeLabel: getLabel,
makeLogo: getLogo,
makeBadgeData: getBadgeData,
} = require('./lib/badge-data');
var semver = require('semver');
var serverStartTime = new Date((new Date()).toGMTString());
var validTemplates = ['default', 'plastic', 'flat', 'flat-square', 'social'];
var darkBackgroundTemplates = ['default', 'flat', 'flat-square'];
var logos = loadLogos();
analyticsAutoLoad();
camp.ajax.on('analytics/v1', function(json, end) { end(getAnalytics()); });
@@ -762,7 +764,6 @@ cache(function(data, match, sendBadge, request) {
apiUrl += '/branch/' + branch;
}
var badgeData = getBadgeData('build', data);
badgeData.logo = badgeData.logo || logos['appveyor'];
request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) {
if (err != null) {
badgeData.text[1] = 'inaccessible';
@@ -1095,7 +1096,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = 'https://gratipay.com/' + user + '/public.json';
var badgeData = getBadgeData('receives', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.gratipay;
badgeData.logo = getLogo('gratipay', data);
}
request(apiUrl, function dealWithData(err, res, buffer) {
if (err != null) {
@@ -3230,7 +3231,6 @@ cache(function (data, match, sendBadge, request) {
}
try {
badgeData.colorscheme = 'brightgreen';
badgeData.logo = logos.sourcegraph;
var data = JSON.parse(buffer);
badgeData.text[1] = data.value;
sendBadge(format, badgeData);
@@ -3250,7 +3250,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo + '/tags';
var badgeData = getBadgeData('tag', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3283,7 +3283,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo + '/contributors?page=1&per_page=1&anon=' + (!!isAnon);
var badgeData = getBadgeData('contributors', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3321,7 +3321,7 @@ cache(function(data, match, sendBadge, request) {
apiUrl = apiUrl + '/latest';
}
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3357,7 +3357,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo + '/compare/' + version + '...master';
var badgeData = getBadgeData('commits since ' + version, data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3401,7 +3401,7 @@ cache(function(data, match, sendBadge, request) {
}
var badgeData = getBadgeData('downloads', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3474,7 +3474,7 @@ cache(function(data, match, sendBadge, request) {
var targetText = isPR? 'pull requests': 'issues';
var badgeData = getBadgeData(leftClassText + labelText + targetText, data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, query, function(err, res, buffer) {
if (err != null) {
@@ -3504,7 +3504,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo;
var badgeData = getBadgeData('forks', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
badgeData.links = [
'https://github.com/' + user + '/' + repo + '/fork',
'https://github.com/' + user + '/' + repo + '/network',
@@ -3539,7 +3539,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo;
var badgeData = getBadgeData('stars', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
badgeData.links = [
'https://github.com/' + user + '/' + repo,
'https://github.com/' + user + '/' + repo + '/stargazers',
@@ -3572,7 +3572,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo;
var badgeData = getBadgeData('watchers', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
badgeData.links = [
'https://github.com/' + user + '/' + repo,
'https://github.com/' + user + '/' + repo + '/watchers',
@@ -3604,7 +3604,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/users/' + user;
var badgeData = getBadgeData('followers', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err != null) {
@@ -3633,7 +3633,7 @@ cache(function(data, match, sendBadge, request) {
var apiUrl = githubApiUrl + '/repos/' + user + '/' + repo;
var badgeData = getBadgeData('license', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
// Using our OAuth App secret grants us 5000 req/hour
// instead of the standard 60 req/hour.
@@ -3687,7 +3687,7 @@ cache(function(data, match, sendBadge, request) {
var badgeData = getBadgeData('size', data);
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.github;
badgeData.logo = getLogo('github', data);
}
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
@@ -5266,8 +5266,6 @@ cache(function(data, match, sendBadge, request) {
request(apiUrl, {json: true}, function(err, res, data) {
try {
badgeData.logo = logos['dockbit'];
if (res && (res.statusCode === 404 || data.state === null)) {
badgeData.text[1] = 'not found';
sendBadge(format, badgeData);
@@ -5755,7 +5753,7 @@ cache(function(data, match, sendBadge, request) {
//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.logo = getLogo('twitter', data);
badgeData.links = [
'https://twitter.com/intent/tweet?text=Wow:&url=' + page,
'https://twitter.com/search?q=' + page,
@@ -5780,7 +5778,7 @@ cache(function(data, match, sendBadge, request) {
badgeData.colorscheme = null;
badgeData.colorB = '#55ACEE';
if (badgeData.template === 'social') {
badgeData.logo = badgeData.logo || logos.twitter;
badgeData.logo = getLogo('twitter', data);
}
badgeData.links = [
'https://twitter.com/intent/follow?screen_name=' + user,
@@ -5925,10 +5923,6 @@ cache(function(data, match, sendBadge, request) {
var badgeData = getBadgeData('chat', data);
badgeData.text[1] = 'on gitter';
badgeData.colorscheme = 'brightgreen';
if (darkBackgroundTemplates.some(function(t) { return t === badgeData.template; })) {
badgeData.logo = badgeData.logo || logos['gitter-white'];
badgeData.logoWidth = 9;
}
sendBadge(format, badgeData);
}));
@@ -6089,8 +6083,6 @@ cache(function(data, match, sendBadge, request) {
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);
@@ -6892,7 +6884,7 @@ function(data, match, end, ask) {
badgeData.colorscheme = color;
}
}
if (data.style && validTemplates.indexOf(data.style) > -1) {
if (isValidStyle(data.style)) {
badgeData.template = data.style;
}
badge(badgeData, makeSend(format, ask.res, end));
@@ -6972,53 +6964,6 @@ function escapeFormatSlashes(t) {
}
function sixHex(s) { return /^[0-9a-fA-F]{6}$/.test(s); }
function getLabel(label, data) {
return data.label || label;
}
function colorParam(color) { return (sixHex(color) ? '#' : '') + color; }
// data (URL query) can include `label`, `style`, `logo`, `logoWidth`, `link`,
// `colorA`, `colorB`.
// 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;
}
if (data.colorA !== undefined) {
data.colorA = colorParam(data.colorA);
}
if (data.colorB !== undefined) {
data.colorB = colorParam(data.colorB);
}
return {
text: [label, 'n/a'],
colorscheme: 'lightgrey',
template: template,
logo: data.logo,
logoWidth: +data.logoWidth,
links: data.link,
colorA: data.colorA,
colorB: data.colorB
};
}
function makeSend(format, askres, end) {
if (format === 'svg') {