diff --git a/badge-maker/lib/constants.js b/badge-maker/lib/constants.js new file mode 100644 index 0000000000000..82f8b1d301ab5 --- /dev/null +++ b/badge-maker/lib/constants.js @@ -0,0 +1,5 @@ +const DEFAULT_LOGO_HEIGHT = 14 + +module.exports = { + DEFAULT_LOGO_HEIGHT, +} diff --git a/badge-maker/lib/make-badge.js b/badge-maker/lib/make-badge.js index fd68199e64be9..bb96c62867c9c 100644 --- a/badge-maker/lib/make-badge.js +++ b/badge-maker/lib/make-badge.js @@ -3,6 +3,7 @@ const { normalizeColor, toSvgColor } = require('./color') const badgeRenderers = require('./badge-renderers') const { stripXmlWhitespace } = require('./xml') +const { DEFAULT_LOGO_HEIGHT } = require('./constants') /* note: makeBadge() is fairly thinly wrapped so if we are making changes here @@ -17,6 +18,7 @@ module.exports = function makeBadge({ labelColor, logo, logoPosition, + logoSize, logoWidth, links = ['', ''], }) { @@ -45,7 +47,7 @@ module.exports = function makeBadge({ throw new Error(`Unknown badge style: '${style}'`) } - logoWidth = +logoWidth || (logo ? 14 : 0) + logoWidth = +logoWidth || (logo ? DEFAULT_LOGO_HEIGHT : 0) return stripXmlWhitespace( render({ @@ -55,6 +57,7 @@ module.exports = function makeBadge({ logo, logoPosition, logoWidth, + logoSize, logoPadding: logo && label.length ? 3 : 0, color: toSvgColor(color), labelColor: toSvgColor(labelColor), diff --git a/core/base-service/base.spec.js b/core/base-service/base.spec.js index 8f63654679bec..73b2085961132 100644 --- a/core/base-service/base.spec.js +++ b/core/base-service/base.spec.js @@ -373,6 +373,8 @@ describe('BaseService', function () { namedLogo: undefined, logo: undefined, logoWidth: undefined, + logoHeight: undefined, + logoSize: undefined, logoPosition: undefined, links: [], labelColor: undefined, diff --git a/core/base-service/coalesce-badge.js b/core/base-service/coalesce-badge.js index d07214b61d8e5..925e466e1dd96 100644 --- a/core/base-service/coalesce-badge.js +++ b/core/base-service/coalesce-badge.js @@ -2,7 +2,8 @@ import { decodeDataUrlFromQueryParam, prepareNamedLogo, } from '../../lib/logos.js' -import { svg2base64 } from '../../lib/svg-helpers.js' +import { svg2base64, getIconSize } from '../../lib/svg-helpers.js' +import { DEFAULT_LOGO_HEIGHT } from '../../badge-maker/lib/constants.js' import coalesce from './coalesce.js' import toArray from './to-array.js' @@ -55,7 +56,9 @@ export default function coalesceBadge( } = overrides let { logoWidth: overrideLogoWidth, + logoHeight: overrideLogoHeight, logoPosition: overrideLogoPosition, + logoSize: overrideLogoSize, color: overrideColor, labelColor: overrideLabelColor, } = overrides @@ -76,6 +79,7 @@ export default function coalesceBadge( overrideLabelColor = `${overrideLabelColor}` } overrideLogoWidth = +overrideLogoWidth || undefined + overrideLogoHeight = +overrideLogoHeight || undefined overrideLogoPosition = +overrideLogoPosition || undefined const { @@ -87,7 +91,9 @@ export default function coalesceBadge( logoSvg: serviceLogoSvg, namedLogo: serviceNamedLogo, logoColor: serviceLogoColor, + logoSize: serviceLogoSize, logoWidth: serviceLogoWidth, + logoHeight: serviceLogoHeight, logoPosition: serviceLogoPosition, link: serviceLink, cacheSeconds: serviceCacheSeconds, @@ -119,7 +125,13 @@ export default function coalesceBadge( style = 'flat' } - let namedLogo, namedLogoColor, logoWidth, logoPosition, logoSvgBase64 + let namedLogo, + namedLogoColor, + logoSize, + logoWidth, + logoHeight, + logoPosition, + logoSvgBase64 if (overrideLogo) { // `?logo=` could be a named logo or encoded svg. const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrideLogo) @@ -133,7 +145,9 @@ export default function coalesceBadge( } // If the logo has been overridden it does not make sense to inherit the // original width or position. + logoSize = overrideLogoSize logoWidth = overrideLogoWidth + logoHeight = overrideLogoHeight logoPosition = overrideLogoPosition } else { if (serviceLogoSvg) { @@ -145,13 +159,23 @@ export default function coalesceBadge( ) namedLogoColor = coalesce(overrideLogoColor, serviceLogoColor) } + logoSize = coalesce(overrideLogoSize, serviceLogoSize) logoWidth = coalesce(overrideLogoWidth, serviceLogoWidth) + logoHeight = coalesce(overrideLogoHeight, serviceLogoHeight) logoPosition = coalesce(overrideLogoPosition, serviceLogoPosition) } if (namedLogo) { + const iconSize = getIconSize(String(namedLogo).toLowerCase()) + + if (!logoWidth && iconSize && logoSize === 'auto') { + logoWidth = + (iconSize.width / iconSize.height) * (logoHeight || DEFAULT_LOGO_HEIGHT) + } + logoSvgBase64 = prepareNamedLogo({ name: namedLogo, color: namedLogoColor, + size: logoSize, style, }) } @@ -178,7 +202,9 @@ export default function coalesceBadge( namedLogo, logo: logoSvgBase64, logoWidth, + logoHeight, logoPosition, + logoSize, links: toArray(overrideLink || serviceLink), cacheLengthSeconds: coalesce(serviceCacheSeconds, defaultCacheSeconds), } diff --git a/core/base-service/coalesce-badge.spec.js b/core/base-service/coalesce-badge.spec.js index 9cf4a7624f77a..820ab29d17007 100644 --- a/core/base-service/coalesce-badge.spec.js +++ b/core/base-service/coalesce-badge.spec.js @@ -260,6 +260,20 @@ describe('coalesceBadge', function () { }) }) + describe('Logo size', function () { + it('overrides the logoSize', function () { + expect(coalesceBadge({ logoSize: 'auto' }, {}, {})).to.include({ + logoSize: 'auto', + }) + }) + + it('applies the logo size', function () { + expect( + coalesceBadge({}, { namedLogo: 'npm', logoSize: 'auto' }, {}) + ).to.include({ logoSize: 'auto' }) + }) + }) + describe('Logo width', function () { it('overrides the logoWidth', function () { expect(coalesceBadge({ logoWidth: 20 }, {}, {})).to.include({ @@ -274,6 +288,20 @@ describe('coalesceBadge', function () { }) }) + describe('Logo height', function () { + it('overrides the logoHeight', function () { + expect(coalesceBadge({ logoHeight: 10 }, {}, {})).to.include({ + logoHeight: 10, + }) + }) + + it('applies the logo height', function () { + expect( + coalesceBadge({}, { namedLogo: 'npm', logoHeight: 10 }, {}) + ).to.include({ logoHeight: 10 }) + }) + }) + describe('Logo position', function () { it('overrides the logoPosition', function () { expect(coalesceBadge({ logoPosition: -10 }, {}, {})).to.include({ diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js index 92847b71f52ae..d98692a5b5749 100644 --- a/core/base-service/openapi.js +++ b/core/base-service/openapi.js @@ -9,6 +9,9 @@ const globalParamRefs = [ { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, + { $ref: '#/components/parameters/logoWidth' }, + { $ref: '#/components/parameters/logoHeight' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -270,6 +273,37 @@ function category2openapi(category, services) { }, example: 'violet', }, + logoSize: { + name: 'logoSize', + in: 'query', + required: false, + description: + "Make icons adaptively resize by setting `auto`. It's useful for some wider logos like `amd` and `amg`.", + schema: { + type: 'string', + }, + example: 'auto', + }, + logoWidth: { + name: 'logoWidth', + in: 'query', + required: false, + description: 'The width of the logo, default to `14`', + schema: { + type: 'string', + }, + example: '20', + }, + logoHeight: { + name: 'logoHeight', + in: 'query', + required: false, + description: 'The height of the logo, default to `14`', + schema: { + type: 'string', + }, + example: '12', + }, label: { name: 'label', in: 'query', diff --git a/core/base-service/openapi.spec.js b/core/base-service/openapi.spec.js index 0577f078e682c..1d11ae3e22e91 100644 --- a/core/base-service/openapi.spec.js +++ b/core/base-service/openapi.spec.js @@ -111,6 +111,37 @@ const expected = { schema: { type: 'string' }, example: 'violet', }, + logoSize: { + name: 'logoSize', + in: 'query', + required: false, + description: + "Make icons adaptively resize by setting `auto`. It's useful for some wider logos like `amd` and `amg`.", + schema: { + type: 'string', + }, + example: 'auto', + }, + logoWidth: { + name: 'logoWidth', + in: 'query', + required: false, + description: 'The width of the logo, default to `14`', + schema: { + type: 'string', + }, + example: '20', + }, + logoHeight: { + name: 'logoHeight', + in: 'query', + required: false, + description: 'The height of the logo, default to `14`', + schema: { + type: 'string', + }, + example: '12', + }, label: { name: 'label', in: 'query', @@ -176,6 +207,9 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, + { $ref: '#/components/parameters/logoWidth' }, + { $ref: '#/components/parameters/logoHeight' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -231,6 +265,9 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, + { $ref: '#/components/parameters/logoWidth' }, + { $ref: '#/components/parameters/logoHeight' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -285,6 +322,9 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, + { $ref: '#/components/parameters/logoWidth' }, + { $ref: '#/components/parameters/logoHeight' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -331,6 +371,9 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, + { $ref: '#/components/parameters/logoWidth' }, + { $ref: '#/components/parameters/logoHeight' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, diff --git a/lib/load-simple-icons.js b/lib/load-simple-icons.js index b2fa89b1c457b..e60e74a106b1d 100644 --- a/lib/load-simple-icons.js +++ b/lib/load-simple-icons.js @@ -1,5 +1,4 @@ import * as originalSimpleIcons from 'simple-icons/icons' -import { svg2base64 } from './svg-helpers.js' function loadSimpleIcons() { const simpleIcons = {} @@ -16,10 +15,10 @@ function loadSimpleIcons() { const icon = originalSimpleIcons[key] const { title, slug, hex } = icon - icon.base64 = { - default: svg2base64(icon.svg.replace(' iconHeight) { + const path = resetIconPosition(simpleIcons[key].path) + iconSvg = iconSvg + .replace('viewBox="0 0 24 24"', `viewBox="0 0 24 ${iconHeight}"`) + .replace(//, ``) + } } + + return svg2base64(iconSvg) } -function prepareNamedLogo({ name, color, style }) { +function prepareNamedLogo({ name, color, style, size }) { if (typeof name !== 'string') { return undefined } @@ -118,7 +131,8 @@ function prepareNamedLogo({ name, color, style }) { } return ( - getShieldsIcon({ name, color }) || getSimpleIcon({ name, color, style }) + getShieldsIcon({ name, color }) || + getSimpleIcon({ name, color, style, size }) ) } @@ -131,6 +145,7 @@ function makeLogo(defaultNamedLogo, overrides) { name: coalesce(overrides.logo, defaultNamedLogo), color: overrides.logoColor, style: overrides.style, + size: overrides.logoSize, }) } } diff --git a/lib/svg-helpers.js b/lib/svg-helpers.js index e36bf10e1981a..f564ab3555049 100644 --- a/lib/svg-helpers.js +++ b/lib/svg-helpers.js @@ -1,7 +1,38 @@ +import SVGPathCommander from 'svg-path-commander' +import loadSimpleIcons from './load-simple-icons.js' + function svg2base64(svg) { return `data:image/svg+xml;base64,${Buffer.from(svg.trim()).toString( 'base64', )}` } -export { svg2base64 } +function getIconSize(iconKey) { + const simpleIcons = loadSimpleIcons() + + if (!(iconKey in simpleIcons)) { + return undefined + } + + const { + width, + height, + x: x0, + y: y0, + x2: x1, + y2: y1, + } = SVGPathCommander.getPathBBox(simpleIcons[iconKey].path) + + return { width, height, x0, y0, x1, y1 } +} + +function resetIconPosition(path) { + const { x: offsetX, y: offsetY } = SVGPathCommander.getPathBBox(path) + const pathReset = new SVGPathCommander(path) + .transform({ translate: [-offsetX, -offsetY] }) + .toString() + + return pathReset +} + +export { svg2base64, getIconSize, resetIconPosition } diff --git a/package-lock.json b/package-lock.json index 97efc1e9cb25e..f38752cd201f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "semver": "~7.5.4", "simple-icons": "9.11.0", "smol-toml": "1.1.2", + "svg-path-commander": "^2.0.8", "webextension-store-meta": "^1.0.5", "xpath": "~0.0.33" }, @@ -5103,6 +5104,11 @@ "node": ">=14.16" } }, + "node_modules/@thednp/dommatrix": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@thednp/dommatrix/-/dommatrix-2.0.6.tgz", + "integrity": "sha512-DXQq4Rs/akYzeXYGkNy3KiJ4JoD8+SYr1QRWTXtAGoZ0+vJcyBt0aeqA1K4CxPaBaIfKdOTE+Te1HV9sAQ4I4A==" + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -25553,6 +25559,14 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "node_modules/svg-path-commander": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/svg-path-commander/-/svg-path-commander-2.0.8.tgz", + "integrity": "sha512-Yg9CQ8aIt0oeMyCf1WerSLPFIlrDnfAkUEasO6vCGe2NGaz2cb5HrzSAS/EzCh5oS+Q/Ral0I4Szdk+Yb+TeNw==", + "dependencies": { + "@thednp/dommatrix": "^2.0.5" + } + }, "node_modules/svgo": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", diff --git a/package.json b/package.json index 069e6d5cb2718..a9a549af91710 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "semver": "~7.5.4", "simple-icons": "9.11.0", "smol-toml": "1.1.2", + "svg-path-commander": "^2.0.8", "webextension-store-meta": "^1.0.5", "xpath": "~0.0.33" }, diff --git a/services/endpoint/endpoint.service.js b/services/endpoint/endpoint.service.js index 33d4788b02804..97ad0122a737e 100644 --- a/services/endpoint/endpoint.service.js +++ b/services/endpoint/endpoint.service.js @@ -103,6 +103,28 @@ const description = `

the query string. + + logoSize + + Default: none. Same meaning as the query string. + Make icons adaptively resize by setting auto. + It's useful for some wider logos like amd and amg. + + + + logoWidth + + Default: 14. Same meaning as the query string. Can be overridden by + the query string. + + + + logoHeight + + Default: 14. Same meaning as the query string. Can be overridden by + the query string. + + logoPosition