diff --git a/lib/badge-data.js b/lib/badge-data.js index f2931dc8e60c60615c5e830f17a0939ab8997968..96647f97df3adf1c620239708478b8c19960bb2c 100644 --- a/lib/badge-data.js +++ b/lib/badge-data.js @@ -1,9 +1,6 @@ 'use strict' -const { toSvgColor } = require('../gh-badges/lib/color') -const logos = require('./load-logos')() -const simpleIcons = require('./load-simple-icons')() -const { svg2base64, isDataUri } = require('./logo-helper') +const { makeLogo } = require('./logos') function toArray(val) { if (val === undefined) { @@ -15,20 +12,6 @@ function toArray(val) { } } -function prependPrefix(s, prefix) { - if (s === undefined) { - return undefined - } - - s = `${s}` - - if (s.startsWith(prefix)) { - return s - } else { - return prefix + s - } -} - function makeLabel(defaultLabel, overrides) { return `${ overrides.label === undefined @@ -37,51 +20,6 @@ function makeLabel(defaultLabel, overrides) { }` } -function getShieldsIcon(icon = '', color = '') { - icon = typeof icon === 'string' ? icon.toLowerCase() : '' - if (!logos[icon]) { - return undefined - } - color = toSvgColor(color) - return color - ? logos[icon].svg.replace(/fill="(.+?)"/g, `fill="${color}"`) - : logos[icon].base64 -} - -function getSimpleIcon(icon = '', color) { - icon = typeof icon === 'string' ? icon.toLowerCase().replace(/ /g, '-') : '' - if (!simpleIcons[icon]) { - return undefined - } - color = toSvgColor(color) - return color - ? simpleIcons[icon].svg.replace('<svg', `<svg fill="${color}"`) - : simpleIcons[icon].base64 -} - -function makeLogo(defaultNamedLogo, overrides) { - if (overrides.logo === undefined) { - return svg2base64( - getShieldsIcon(defaultNamedLogo, overrides.logoColor) || - getSimpleIcon(defaultNamedLogo, overrides.logoColor) - ) - } - - // +'s are replaced with spaces when used in query params, this returns them to +'s, then removes remaining whitespace - #1546 - const maybeDataUri = prependPrefix(overrides.logo, 'data:') - .replace(/ /g, '+') - .replace(/\s/g, '') - - if (isDataUri(maybeDataUri)) { - return maybeDataUri - } else { - return svg2base64( - getShieldsIcon(overrides.logo, overrides.logoColor) || - getSimpleIcon(overrides.logo, overrides.logoColor) - ) - } -} - // Generate the initial badge data. Pass the URL query parameters, which // override the default label. // @@ -120,8 +58,6 @@ function makeBadgeData(defaultLabel, overrides) { module.exports = { toArray, - prependPrefix, makeLabel, - makeLogo, makeBadgeData, } diff --git a/lib/badge-data.spec.js b/lib/badge-data.spec.js index 783368055225b36137d7f3883860d28755da254c..70d478ded967cc9609941e1da2198717181ebe91 100644 --- a/lib/badge-data.spec.js +++ b/lib/badge-data.spec.js @@ -1,23 +1,9 @@ 'use strict' -const { expect } = require('chai') -const { test, given, forCases } = require('sazerac') -const { - prependPrefix, - makeLabel, - makeLogo, - makeBadgeData, -} = require('./badge-data') +const { test, given } = require('sazerac') +const { makeLabel, makeBadgeData } = require('./badge-data') describe('Badge data helpers', function() { - test(prependPrefix, () => { - given('data:image/svg+xml;base64,PHN2ZyB4bWxu', 'data:').expect( - 'data:image/svg+xml;base64,PHN2ZyB4bWxu' - ) - given('foobar', 'data:').expect('data:foobar') - given(undefined, 'data:').expect(undefined) - }) - test(makeLabel, () => { given('my badge', {}).expect('my badge') given('My bAdge', {}).expect('my badge') @@ -28,22 +14,6 @@ describe('Badge data helpers', function() { given('my badge', { label: '' }).expect('') }) - test(makeLogo, () => { - forCases([ - given('npm', { logo: 'image/svg+xml;base64,PHN2ZyB4bWxu' }), - given('npm', { logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu' }), - given('npm', { logo: 'data:image/svg xml;base64,PHN2ZyB4bWxu' }), - given('npm', { logo: 'data:image/svg+xml;base64,PHN2ZyB\n4bWxu' }), - ]).expect('data:image/svg+xml;base64,PHN2ZyB4bWxu') - forCases([given('npm', { logo: '' }), given(undefined, {})]).expect( - undefined - ) - given('npm', {}).assert( - 'should not be empty', - v => expect(v).not.to.be.empty - ) - }) - test(makeBadgeData, () => { given('my badge', { label: 'no, my badge', diff --git a/lib/load-logos.js b/lib/load-logos.js index f812de8fc33273427b10bf2fb2649a8676829e8b..756793a536cb89d5ed6c57bff5229db1228c748b 100644 --- a/lib/load-logos.js +++ b/lib/load-logos.js @@ -2,7 +2,7 @@ const fs = require('fs') const path = require('path') -const { svg2base64 } = require('./logo-helper') +const { svg2base64 } = require('./svg-helpers') function loadLogos() { // Cache svg logos from disk in base64 string diff --git a/lib/load-simple-icons.js b/lib/load-simple-icons.js index 02729d42c92dac276cd3deffcfded225f750c2cc..6e8a4e8725ca21f283788cfceaa4f195628b7a9f 100644 --- a/lib/load-simple-icons.js +++ b/lib/load-simple-icons.js @@ -1,7 +1,7 @@ 'use strict' const simpleIcons = require('simple-icons') -const { svg2base64 } = require('./logo-helper') +const { svg2base64 } = require('./svg-helpers') function loadSimpleIcons() { Object.keys(simpleIcons).forEach(key => { diff --git a/lib/logo-helper.js b/lib/logo-helper.js deleted file mode 100644 index b31f15814ad94e9fbcad3b8c964f519d048db303..0000000000000000000000000000000000000000 --- a/lib/logo-helper.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -function isDataUri(s) { - return s !== undefined && /^(data:)([^;]+);([^,]+),(.+)$/.test(s) -} - -function svg2base64(svg) { - if (typeof svg !== 'string') { - return undefined - } - // Check if logo is already base64 - return isDataUri(svg) - ? svg - : `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}` -} - -module.exports = { - svg2base64, - isDataUri, -} diff --git a/lib/logo-helper.spec.js b/lib/logo-helper.spec.js deleted file mode 100644 index 2acdad7b36082768ec948419dba17dacb1531bdc..0000000000000000000000000000000000000000 --- a/lib/logo-helper.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -const { test, given, forCases } = require('sazerac') -const { svg2base64, isDataUri } = require('./logo-helper') - -describe('Logo helpers', function() { - test(svg2base64, () => { - given('data:image/svg+xml;base64,PHN2ZyB4bWxu').expect( - 'data:image/svg+xml;base64,PHN2ZyB4bWxu' - ) - given('<svg xmlns="http://www.w3.org/2000/svg"/>').expect( - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=' - ) - given(undefined).expect(undefined) - }) - - test(isDataUri, () => { - given('data:image/svg+xml;base64,PHN2ZyB4bWxu').expect(true) - forCases([given('data:foobar'), given('foobar')]).expect(false) - }) -}) diff --git a/lib/logos.js b/lib/logos.js new file mode 100644 index 0000000000000000000000000000000000000000..50795b0501587563b4fdcb156d0668e6d38102fe --- /dev/null +++ b/lib/logos.js @@ -0,0 +1,100 @@ +'use strict' + +const { toSvgColor } = require('../gh-badges/lib/color') +const coalesce = require('./coalesce') +const { svg2base64 } = require('./svg-helpers') + +const logos = require('./load-logos')() +const simpleIcons = require('./load-simple-icons')() + +function prependPrefix(s, prefix) { + if (s === undefined) { + return undefined + } + + s = `${s}` + + if (s.startsWith(prefix)) { + return s + } else { + return prefix + s + } +} + +function isDataUrl(s) { + return s !== undefined && /^(data:)([^;]+);([^,]+),(.+)$/.test(s) +} + +// +'s are replaced with spaces when used in query params, this returns them +// to +'s, then removes remaining whitespace. +// https://github.com/badges/shields/pull/1546 +function decodeDataUrlFromQueryParam(value) { + if (typeof value !== 'string') { + return undefined + } + const maybeDataUrl = prependPrefix(value, 'data:') + .replace(/ /g, '+') + .replace(/\s/g, '') + return isDataUrl(maybeDataUrl) ? maybeDataUrl : undefined +} + +function getShieldsIcon({ name, color }) { + const key = name.toLowerCase() + + if (!(key in logos)) { + return undefined + } + + const { svg, base64 } = logos[key] + const svgColor = toSvgColor(color) + if (svgColor) { + return svg2base64(svg.replace(/fill="(.+?)"/g, `fill="${svgColor}"`)) + } else { + return base64 + } +} + +function getSimpleIcon({ name, color }) { + const key = name.toLowerCase().replace(/ /g, '-') + + if (!(key in simpleIcons)) { + return undefined + } + + const { svg, base64 } = simpleIcons[key] + const svgColor = toSvgColor(color) + if (svgColor) { + return svg2base64(svg.replace('<svg', `<svg fill="${svgColor}"`)) + } else { + return base64 + } +} + +function prepareNamedLogo({ name, color }) { + if (typeof name !== 'string') { + return undefined + } + + return getShieldsIcon({ name, color }) || getSimpleIcon({ name, color }) +} + +function makeLogo(defaultNamedLogo, overrides) { + const maybeDataUrl = decodeDataUrlFromQueryParam(overrides.logo) + if (maybeDataUrl) { + return maybeDataUrl + } else { + return prepareNamedLogo({ + name: coalesce(overrides.logo, defaultNamedLogo), + color: overrides.logoColor, + }) + } +} + +module.exports = { + prependPrefix, + isDataUrl, + decodeDataUrlFromQueryParam, + prepareNamedLogo, + getShieldsIcon, + makeLogo, +} diff --git a/lib/logos.spec.js b/lib/logos.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6f9f22d219677e291f4516d4276a169ac93156aa --- /dev/null +++ b/lib/logos.spec.js @@ -0,0 +1,50 @@ +'use strict' + +const { test, given, forCases } = require('sazerac') +const { + prependPrefix, + isDataUrl, + prepareNamedLogo, + makeLogo, +} = require('./logos') + +describe('Logo helpers', function() { + test(prependPrefix, () => { + given('data:image/svg+xml;base64,PHN2ZyB4bWxu', 'data:').expect( + 'data:image/svg+xml;base64,PHN2ZyB4bWxu' + ) + given('foobar', 'data:').expect('data:foobar') + given(undefined, 'data:').expect(undefined) + }) + + test(isDataUrl, () => { + given('data:image/svg+xml;base64,PHN2ZyB4bWxu').expect(true) + forCases([given('data:foobar'), given('foobar')]).expect(false) + }) + + test(prepareNamedLogo, () => { + // These two strings are identical except for characters 159-165 which + // differ. + given({ name: 'github' }).expect( + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMTIgMTIgNDAgNDAiPgo8cGF0aCBmaWxsPSIjMzMzMzMzIiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjIgYy01LjMsMS4xLTYuNC0yLjYtNi40LTIuNkMyMCw0MS42LDE4LjgsNDEsMTguOCw0MWMtMS43LTEuMiwwLjEtMS4xLDAuMS0xLjFjMS45LDAuMSwyLjksMiwyLjksMmMxLjcsMi45LDQuNSwyLjEsNS41LDEuNiBjMC4yLTEuMiwwLjctMi4xLDEuMi0yLjZjLTQuMi0wLjUtOC43LTIuMS04LjctOS40YzAtMi4xLDAuNy0zLjcsMi01LjFjLTAuMi0wLjUtMC44LTIuNCwwLjItNWMwLDAsMS42LTAuNSw1LjIsMiBjMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQgYzAuNywwLjYsMS4zLDEuNywxLjMsMy41YzAsMi42LDAsNC42LDAsNS4yYzAsMC41LDAuNCwxLjEsMS4zLDAuOWM3LjUtMi42LDEzLTkuNywxMy0xOC4xQzUxLDIxLjksNDIuNSwxMy40LDMyLDEzLjR6Ii8+Cjwvc3ZnPgo=' + ) + given({ name: 'github', color: 'blue' }).expect( + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMTIgMTIgNDAgNDAiPgo8cGF0aCBmaWxsPSIjMDA3ZWM2IiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjIgYy01LjMsMS4xLTYuNC0yLjYtNi40LTIuNkMyMCw0MS42LDE4LjgsNDEsMTguOCw0MWMtMS43LTEuMiwwLjEtMS4xLDAuMS0xLjFjMS45LDAuMSwyLjksMiwyLjksMmMxLjcsMi45LDQuNSwyLjEsNS41LDEuNiBjMC4yLTEuMiwwLjctMi4xLDEuMi0yLjZjLTQuMi0wLjUtOC43LTIuMS04LjctOS40YzAtMi4xLDAuNy0zLjcsMi01LjFjLTAuMi0wLjUtMC44LTIuNCwwLjItNWMwLDAsMS42LTAuNSw1LjIsMiBjMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQgYzAuNywwLjYsMS4zLDEuNywxLjMsMy41YzAsMi42LDAsNC42LDAsNS4yYzAsMC41LDAuNCwxLjEsMS4zLDAuOWM3LjUtMi42LDEzLTkuNywxMy0xOC4xQzUxLDIxLjksNDIuNSwxMy40LDMyLDEzLjR6Ii8+Cjwvc3ZnPgo=' + ) + }) + + test(makeLogo, () => { + forCases([ + given('npm', { logo: 'image/svg+xml;base64,PHN2ZyB4bWxu' }), + given('npm', { logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu' }), + given('npm', { logo: 'data:image/svg xml;base64,PHN2ZyB4bWxu' }), + given('npm', { logo: 'data:image/svg+xml;base64,PHN2ZyB\n4bWxu' }), + ]).expect('data:image/svg+xml;base64,PHN2ZyB4bWxu') + forCases([given('npm', { logo: '' }), given(undefined, {})]).expect( + undefined + ) + given('github', {}).expect( + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMTIgMTIgNDAgNDAiPgo8cGF0aCBmaWxsPSIjMzMzMzMzIiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjIgYy01LjMsMS4xLTYuNC0yLjYtNi40LTIuNkMyMCw0MS42LDE4LjgsNDEsMTguOCw0MWMtMS43LTEuMiwwLjEtMS4xLDAuMS0xLjFjMS45LDAuMSwyLjksMiwyLjksMmMxLjcsMi45LDQuNSwyLjEsNS41LDEuNiBjMC4yLTEuMiwwLjctMi4xLDEuMi0yLjZjLTQuMi0wLjUtOC43LTIuMS04LjctOS40YzAtMi4xLDAuNy0zLjcsMi01LjFjLTAuMi0wLjUtMC44LTIuNCwwLjItNWMwLDAsMS42LTAuNSw1LjIsMiBjMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQgYzAuNywwLjYsMS4zLDEuNywxLjMsMy41YzAsMi42LDAsNC42LDAsNS4yYzAsMC41LDAuNCwxLjEsMS4zLDAuOWM3LjUtMi42LDEzLTkuNywxMy0xOC4xQzUxLDIxLjksNDIuNSwxMy40LDMyLDEzLjR6Ii8+Cjwvc3ZnPgo=' + ) + }) +}) diff --git a/lib/svg-helpers.js b/lib/svg-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..7011c156aed7245f54217acd7a73c9d976ccac08 --- /dev/null +++ b/lib/svg-helpers.js @@ -0,0 +1,7 @@ +'use strict' + +function svg2base64(svg) { + return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}` +} + +module.exports = { svg2base64 } diff --git a/lib/svg-helpers.spec.js b/lib/svg-helpers.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..517794bc474f6485538f54fbba449fdd824a5381 --- /dev/null +++ b/lib/svg-helpers.spec.js @@ -0,0 +1,12 @@ +'use strict' + +const { test, given } = require('sazerac') +const { svg2base64 } = require('./svg-helpers') + +describe('SVG helpers', function() { + test(svg2base64, () => { + given('<svg xmlns="http://www.w3.org/2000/svg"/>').expect( + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=' + ) + }) +}) diff --git a/services/base.js b/services/base.js index 665aac07faaf8b4ca65e34cf6ab2de9743fdde08..664cd307cc5864a2d97369a376ff4a666a7a05ff 100644 --- a/services/base.js +++ b/services/base.js @@ -14,7 +14,12 @@ const { const coalesce = require('../lib/coalesce') const validate = require('../lib/validate') const { checkErrorResponse } = require('../lib/error-helper') -const { makeLogo, toArray } = require('../lib/badge-data') +const { toArray } = require('../lib/badge-data') +const { svg2base64 } = require('../lib/svg-helpers') +const { + decodeDataUrlFromQueryParam, + prepareNamedLogo, +} = require('../lib/logos') const trace = require('./trace') const { validateExample, transformExample } = require('./transform-example') const { assertValidCategory } = require('./categories') @@ -24,7 +29,7 @@ const defaultBadgeDataSchema = Joi.object({ label: Joi.string(), color: Joi.string(), labelColor: Joi.string(), - logo: Joi.string(), + namedLogo: Joi.string(), }).required() const serviceDataSchema = Joi.object({ @@ -38,10 +43,30 @@ const serviceDataSchema = Joi.object({ // Generally services should not use these options, which are provided to // support the Endpoint badge. labelColor: Joi.string(), + namedLogo: Joi.string(), + logoSvg: Joi.string(), + logoColor: Joi.forbidden(), + logoWidth: Joi.forbidden(), + logoPosition: Joi.forbidden(), cacheLengthSeconds: Joi.number() .integer() .min(0), -}).required() +}) + .oxor('namedLogo', 'logoSvg') + .when( + Joi.alternatives().try( + Joi.object({ namedLogo: Joi.string().required() }).unknown(), + Joi.object({ logoSvg: Joi.string().required() }).unknown() + ), + { + then: Joi.object({ + logoColor: Joi.string(), + logoWidth: Joi.number(), + logoPosition: Joi.number(), + }), + } + ) + .required() class BaseService { constructor({ sendAndCacheRequest }, { handleInternalErrors }) { @@ -327,24 +352,56 @@ class BaseService { } // Translate modern badge data to the legacy schema understood by the badge - // maker. + // maker. Allow the user to override the label, color, logo, etc. through + // the query string. Provide support for most badge options via + // `serviceData` so the Endpoint badge can specify logos and colors, though + // allow that the user's logo or color to take precedence. A notable + // exception is the case of errors. When the service specifies that an error + // has occurred, the user's requested color does not override the error color. + // + // Logos are resolved in this manner: + // + // 1. When `?logo=` contains the name of one of the Shields logos, or contains + // base64-encoded SVG, that logo is used. In the case of a named logo, when + // a `&logoColor=` is specified, that color is used. Otherwise the default + // color is used. `logoColor` will not be applied to a custom + // (base64-encoded) logo; if a custom color is desired the logo should be + // recolored prior to making the request. The appearance of the logo can be + // customized using `logoWidth`, and in the case of the popout badge, + // `logoPosition`. When `?logo=` is specified, any logo-related parameters + // specified dynamically by the service, or by default in the service, are + // ignored. + // 2. The second precedence is the dynamic logo returned by a service. This is + // used only by the Endpoint badge. The `logoColor` can be overridden by the + // query string. + // 3. In the case of the `social` style only, the last precedence is the + // service's default logo. The `logoColor` can be overridden by the query + // string. static _makeBadgeData(overrides, serviceData) { const { style, label: overrideLabel, - logo: overrideLogo, logoColor: overrideLogoColor, - logoWidth: overrideLogoWidth, link: overrideLink, } = overrides // Scoutcamp converts numeric query params to numbers. Convert them back. - let { colorB: overrideColor, colorA: overrideLabelColor } = overrides + let { + colorB: overrideColor, + colorA: overrideLabelColor, + logoWidth: overrideLogoWidth, + logoPosition: overrideLogoPosition, + } = overrides if (typeof overrideColor === 'number') { overrideColor = `${overrideColor}` } if (typeof overrideLabelColor === 'number') { overrideLabelColor = `${overrideLabelColor}` } + overrideLogoWidth = +overrideLogoWidth || undefined + overrideLogoPosition = +overrideLogoPosition || undefined + // `?logo=` could be a named logo or encoded svg. Split up these cases. + const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrides.logo) + const overrideNamedLogo = overrideLogoSvgBase64 ? undefined : overrides.logo const { isError, @@ -352,19 +409,41 @@ class BaseService { message: serviceMessage, color: serviceColor, labelColor: serviceLabelColor, + logoSvg: serviceLogoSvg, + namedLogo: serviceNamedLogo, + logoColor: serviceLogoColor, + logoWidth: serviceLogoWidth, + logoPosition: serviceLogoPosition, link: serviceLink, cacheLengthSeconds: serviceCacheLengthSeconds, } = serviceData + const serviceLogoSvgBase64 = serviceLogoSvg + ? svg2base64(serviceLogoSvg) + : undefined const { color: defaultColor, - logo: defaultLogo, + namedLogo: defaultNamedLogo, label: defaultLabel, labelColor: defaultLabelColor, } = this.defaultBadgeData const defaultCacheLengthSeconds = this._cacheLength - const badgeData = { + const namedLogoSvgBase64 = prepareNamedLogo({ + name: coalesce( + overrideNamedLogo, + serviceNamedLogo, + style === 'social' ? defaultNamedLogo : undefined + ), + color: coalesce( + overrideLogoColor, + // If the logo has been overridden it does not make sense to inherit + // the color. + overrideNamedLogo ? undefined : serviceLogoColor + ), + }) + + return { text: [ // Use `coalesce()` to support empty labels and messages, as in the // static badge. @@ -385,19 +464,27 @@ class BaseService { defaultLabelColor ), template: style, - logo: makeLogo(style === 'social' ? defaultLogo : undefined, { - logo: overrideLogo, - logoColor: overrideLogoColor, - }), - logoWidth: +overrideLogoWidth, + logo: coalesce( + overrideLogoSvgBase64, + serviceLogoSvgBase64, + namedLogoSvgBase64 + ), + logoWidth: coalesce( + overrideLogoWidth, + // If the logo has been overridden it does not make sense to inherit + // the width or position. + overrideNamedLogo ? undefined : serviceLogoWidth + ), + logoPosition: coalesce( + overrideLogoPosition, + overrideNamedLogo ? undefined : serviceLogoPosition + ), links: toArray(overrideLink || serviceLink), cacheLengthSeconds: coalesce( serviceCacheLengthSeconds, defaultCacheLengthSeconds ), } - - return badgeData } static register({ camp, handleRequest, githubApiProvider }, serviceConfig) { diff --git a/services/base.spec.js b/services/base.spec.js index ddb31184dd68068426ab369e54b487ff50619c73..506d51442d9e18ece7af07614d0817e36b3ee369 100644 --- a/services/base.spec.js +++ b/services/base.spec.js @@ -4,6 +4,7 @@ const Joi = require('joi') const { expect } = require('chai') const { test, given, forCases } = require('sazerac') const sinon = require('sinon') +const { getShieldsIcon } = require('../lib/logos') const trace = require('./trace') const { @@ -33,7 +34,7 @@ class DummyService extends BaseService { } static get defaultBadgeData() { - return { label: 'cat' } + return { label: 'cat', namedLogo: 'appveyor' } } static get examples() { @@ -414,23 +415,65 @@ describe('BaseService', function() { }) it('overrides the logo', function() { - const expLogo = - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMTIgMTIgNDAgNDAiPgo8cGF0aCBmaWxsPSIjMzMzMzMzIiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjIgYy01LjMsMS4xLTYuNC0yLjYtNi40LTIuNkMyMCw0MS42LDE4LjgsNDEsMTguOCw0MWMtMS43LTEuMiwwLjEtMS4xLDAuMS0xLjFjMS45LDAuMSwyLjksMiwyLjksMmMxLjcsMi45LDQuNSwyLjEsNS41LDEuNiBjMC4yLTEuMiwwLjctMi4xLDEuMi0yLjZjLTQuMi0wLjUtOC43LTIuMS04LjctOS40YzAtMi4xLDAuNy0zLjcsMi01LjFjLTAuMi0wLjUtMC44LTIuNCwwLjItNWMwLDAsMS42LTAuNSw1LjIsMiBjMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQgYzAuNywwLjYsMS4zLDEuNywxLjMsMy41YzAsMi42LDAsNC42LDAsNS4yYzAsMC41LDAuNCwxLjEsMS4zLDAuOWM3LjUtMi42LDEzLTkuNywxMy0xOC4xQzUxLDIxLjksNDIuNSwxMy40LDMyLDEzLjR6Ii8+Cjwvc3ZnPgo=' const badgeData = DummyService._makeBadgeData( - { logo: 'github', style: 'social' }, - {} + { logo: 'github' }, + { namedLogo: 'appveyor' } ) - expect(badgeData.logo).to.equal(expLogo) + // .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`. + expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and + .not.be.empty }) - it('overrides the logo with color', function() { - const expLogo = - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMTIgMTIgNDAgNDAiPgo8cGF0aCBmaWxsPSIjMDA3ZWM2IiBkPSJNMzIsMTMuNGMtMTAuNSwwLTE5LDguNS0xOSwxOWMwLDguNCw1LjUsMTUuNSwxMywxOGMxLDAuMiwxLjMtMC40LDEuMy0wLjljMC0wLjUsMC0xLjcsMC0zLjIgYy01LjMsMS4xLTYuNC0yLjYtNi40LTIuNkMyMCw0MS42LDE4LjgsNDEsMTguOCw0MWMtMS43LTEuMiwwLjEtMS4xLDAuMS0xLjFjMS45LDAuMSwyLjksMiwyLjksMmMxLjcsMi45LDQuNSwyLjEsNS41LDEuNiBjMC4yLTEuMiwwLjctMi4xLDEuMi0yLjZjLTQuMi0wLjUtOC43LTIuMS04LjctOS40YzAtMi4xLDAuNy0zLjcsMi01LjFjLTAuMi0wLjUtMC44LTIuNCwwLjItNWMwLDAsMS42LTAuNSw1LjIsMiBjMS41LTAuNCwzLjEtMC43LDQuOC0wLjdjMS42LDAsMy4zLDAuMiw0LjcsMC43YzMuNi0yLjQsNS4yLTIsNS4yLTJjMSwyLjYsMC40LDQuNiwwLjIsNWMxLjIsMS4zLDIsMywyLDUuMWMwLDcuMy00LjUsOC45LTguNyw5LjQgYzAuNywwLjYsMS4zLDEuNywxLjMsMy41YzAsMi42LDAsNC42LDAsNS4yYzAsMC41LDAuNCwxLjEsMS4zLDAuOWM3LjUtMi42LDEzLTkuNywxMy0xOC4xQzUxLDIxLjksNDIuNSwxMy40LDMyLDEzLjR6Ii8+Cjwvc3ZnPgo=' + it('overrides the logo with a color', function() { const badgeData = DummyService._makeBadgeData( { logo: 'github', logoColor: 'blue' }, - {} + { namedLogo: 'appveyor' } ) - expect(badgeData.logo).to.equal(expLogo) + expect(badgeData.logo).to.equal( + getShieldsIcon({ name: 'github', color: 'blue' }) + ).and.not.be.empty + }) + + it("when the logo is overridden, it ignores the service's logo color, position, and width", function() { + const badgeData = DummyService._makeBadgeData( + { logo: 'github' }, + { + namedLogo: 'appveyor', + logoColor: 'red', + logoPosition: -3, + logoWidth: 100, + } + ) + expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and + .not.be.empty + }) + + it("overrides the service logo's color", function() { + const badgeData = DummyService._makeBadgeData( + { logoColor: 'blue' }, + { namedLogo: 'github', logoColor: 'red' } + ) + expect(badgeData.logo).to.equal( + getShieldsIcon({ name: 'github', color: 'blue' }) + ).and.not.be.empty + }) + + it('overrides the logo with custom svg', function() { + const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu' + const badgeData = DummyService._makeBadgeData( + { logo: logoSvg }, + { namedLogo: 'appveyor' } + ) + expect(badgeData.logo).to.equal(logoSvg) + }) + + it('ignores the color when custom svg is provided', function() { + const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu' + const badgeData = DummyService._makeBadgeData( + { logo: logoSvg, logoColor: 'brightgreen' }, + { namedLogo: 'appveyor' } + ) + expect(badgeData.logo).to.equal(logoSvg) }) it('overrides the logoWidth', function() { @@ -438,6 +481,11 @@ describe('BaseService', function() { expect(badgeData.logoWidth).to.equal(20) }) + it('overrides the logoPosition', function() { + const badgeData = DummyService._makeBadgeData({ logoPosition: -10 }, {}) + expect(badgeData.logoPosition).to.equal(-10) + }) + it('overrides the links', function() { const badgeData = DummyService._makeBadgeData( { link: 'https://circleci.com/gh/badges/daily-tests' }, @@ -491,6 +539,42 @@ describe('BaseService', function() { expect(badgeData.color).to.equal('red') }) + it('applies the named logo', function() { + const badgeData = DummyService._makeBadgeData( + {}, + { namedLogo: 'github' } + ) + // .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`. + expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and + .not.to.be.empty + }) + + it('applies the named logo with color', function() { + const badgeData = DummyService._makeBadgeData( + {}, + { namedLogo: 'github', logoColor: 'blue' } + ) + expect(badgeData.logo).to.equal( + getShieldsIcon({ name: 'github', color: 'blue' }) + ).and.not.to.be.empty + }) + + it('applies the logo width', function() { + const badgeData = DummyService._makeBadgeData( + {}, + { namedLogo: 'github', logoWidth: 275 } + ) + expect(badgeData.logoWidth).to.equal(275) + }) + + it('applies the logo position', function() { + const badgeData = DummyService._makeBadgeData( + {}, + { namedLogo: 'github', logoPosition: -10 } + ) + expect(badgeData.logoPosition).to.equal(-10) + }) + it('applies the service label color', function() { const badgeData = DummyService._makeBadgeData({}, { labelColor: 'red' }) expect(badgeData.labelColor).to.equal('red') @@ -512,6 +596,17 @@ describe('BaseService', function() { const badgeData = DummyService._makeBadgeData({}, {}) expect(badgeData.labelColor).to.be.undefined }) + + it('when not a social badge, ignores the default named logo', function() { + const badgeData = DummyService._makeBadgeData({}, {}) + expect(badgeData.logo).to.be.undefined + }) + + it('when a social badge, uses the default named logo', function() { + const badgeData = DummyService._makeBadgeData({ style: 'social' }, {}) + expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'appveyor' })) + .and.not.be.empty + }) }) }) @@ -556,7 +651,8 @@ describe('BaseService', function() { color: 'lightgrey', template: undefined, logo: undefined, - logoWidth: NaN, + logoWidth: undefined, + logoPosition: undefined, links: [], labelColor: undefined, cacheLengthSeconds: undefined, diff --git a/services/github/github-pull-request-check-state.service.js b/services/github/github-pull-request-check-state.service.js index 3c2dbba6b0d876452ab199f163b1b4ccfe3bc8ce..7b089bfe73b75c4048245d3fb0e50833acf5e843 100644 --- a/services/github/github-pull-request-check-state.service.js +++ b/services/github/github-pull-request-check-state.service.js @@ -67,7 +67,7 @@ module.exports = class GithubPullRequestCheckState extends GithubAuthService { static get defaultBadgeData() { return { label: 'checks', - logo: 'github', + namedLogo: 'github', } }