From bafc19de97ba6fcac3ed3b638cd89283a1480518 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 4 Apr 2024 01:12:43 +0800 Subject: [PATCH] feat(logos): support auto-sizing mode --- badge-maker/lib/constants.js | 5 ++++ badge-maker/lib/make-badge.js | 5 +++- core/base-service/base.spec.js | 1 + core/base-service/coalesce-badge.js | 22 +++++++++++++-- core/base-service/coalesce-badge.spec.js | 14 ++++++++++ core/base-service/openapi.js | 12 ++++++++ core/base-service/openapi.spec.js | 13 +++++++++ lib/load-simple-icons.js | 9 +++--- lib/load-simple-icons.spec.js | 2 +- lib/logos.js | 35 ++++++++++++++++++------ lib/logos.spec.js | 8 ++++++ lib/svg-helpers.js | 33 +++++++++++++++++++++- package-lock.json | 18 ++++++++++++ package.json | 1 + 14 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 badge-maker/lib/constants.js 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 575f0386cc9db..e5878c7729438 100644 --- a/core/base-service/base.spec.js +++ b/core/base-service/base.spec.js @@ -377,6 +377,7 @@ describe('BaseService', function () { namedLogo: undefined, logo: undefined, logoWidth: 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..325745c498ed7 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' @@ -56,6 +57,7 @@ export default function coalesceBadge( let { logoWidth: overrideLogoWidth, logoPosition: overrideLogoPosition, + logoSize: overrideLogoSize, color: overrideColor, labelColor: overrideLabelColor, } = overrides @@ -87,6 +89,7 @@ export default function coalesceBadge( logoSvg: serviceLogoSvg, namedLogo: serviceNamedLogo, logoColor: serviceLogoColor, + logoSize: serviceLogoSize, logoWidth: serviceLogoWidth, logoPosition: serviceLogoPosition, link: serviceLink, @@ -119,7 +122,12 @@ export default function coalesceBadge( style = 'flat' } - let namedLogo, namedLogoColor, logoWidth, logoPosition, logoSvgBase64 + let namedLogo, + namedLogoColor, + logoSize, + logoWidth, + logoPosition, + logoSvgBase64 if (overrideLogo) { // `?logo=` could be a named logo or encoded svg. const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrideLogo) @@ -133,6 +141,7 @@ 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 logoPosition = overrideLogoPosition } else { @@ -145,13 +154,21 @@ export default function coalesceBadge( ) namedLogoColor = coalesce(overrideLogoColor, serviceLogoColor) } + logoSize = coalesce(overrideLogoSize, serviceLogoSize) logoWidth = coalesce(overrideLogoWidth, serviceLogoWidth) logoPosition = coalesce(overrideLogoPosition, serviceLogoPosition) } if (namedLogo) { + const iconSize = getIconSize(String(namedLogo).toLowerCase()) + + if (!logoWidth && iconSize && logoSize === 'auto') { + logoWidth = (iconSize.width / iconSize.height) * DEFAULT_LOGO_HEIGHT + } + logoSvgBase64 = prepareNamedLogo({ name: namedLogo, color: namedLogoColor, + size: logoSize, style, }) } @@ -179,6 +196,7 @@ export default function coalesceBadge( logo: logoSvgBase64, logoWidth, 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..27969c6e0a1a2 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({ diff --git a/core/base-service/openapi.js b/core/base-service/openapi.js index 3eabf9cb99f4f..d6f6c7b5e7551 100644 --- a/core/base-service/openapi.js +++ b/core/base-service/openapi.js @@ -9,6 +9,7 @@ const globalParamRefs = [ { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -140,6 +141,17 @@ function category2openapi({ category, services, sort = false }) { }, example: 'violet', }, + logoSize: { + name: 'logoSize', + in: 'query', + required: false, + description: + 'Make icons adaptively resize by setting `auto`. Useful for some wider logos like `amd` and `amg`. Supported for simple-icons logos only.', + schema: { + type: 'string', + }, + example: 'auto', + }, label: { name: 'label', in: 'query', diff --git a/core/base-service/openapi.spec.js b/core/base-service/openapi.spec.js index ca075f65bf88c..e8911653784e7 100644 --- a/core/base-service/openapi.spec.js +++ b/core/base-service/openapi.spec.js @@ -93,6 +93,17 @@ const expected = { schema: { type: 'string' }, example: 'violet', }, + logoSize: { + name: 'logoSize', + in: 'query', + required: false, + description: + 'Make icons adaptively resize by setting `auto`. Useful for some wider logos like `amd` and `amg`. Supported for simple-icons logos only.', + schema: { + type: 'string', + }, + example: 'auto', + }, label: { name: 'label', in: 'query', @@ -158,6 +169,7 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, { $ref: '#/components/parameters/label' }, { $ref: '#/components/parameters/labelColor' }, { $ref: '#/components/parameters/color' }, @@ -213,6 +225,7 @@ const expected = { { $ref: '#/components/parameters/style' }, { $ref: '#/components/parameters/logo' }, { $ref: '#/components/parameters/logoColor' }, + { $ref: '#/components/parameters/logoSize' }, { $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 +135,8 @@ function prepareNamedLogo({ name, color, style }) { } return ( - getShieldsIcon({ name, color }) || getSimpleIcon({ name, color, style }) + getShieldsIcon({ name, color }) || + getSimpleIcon({ name, color, style, size }) ) } @@ -131,6 +149,7 @@ function makeLogo(defaultNamedLogo, overrides) { name: coalesce(overrides.logo, defaultNamedLogo), color: overrides.logoColor, style: overrides.style, + size: overrides.logoSize, }) } } diff --git a/lib/logos.spec.js b/lib/logos.spec.js index 33b6175e8ec02..12b779a05a42c 100644 --- a/lib/logos.spec.js +++ b/lib/logos.spec.js @@ -57,6 +57,14 @@ describe('Logo helpers', function () { given({ name: 'github', color: 'red' }).expect( 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZTA1ZDQ0IiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0SHViPC90aXRsZT48cGF0aCBkPSJNMTIgLjI5N2MtNi42MyAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDMgMy40MzggOS44IDguMjA1IDExLjM4NS42LjExMy44Mi0uMjU4LjgyLS41NzcgMC0uMjg1LS4wMS0xLjA0LS4wMTUtMi4wNC0zLjMzOC43MjQtNC4wNDItMS42MS00LjA0Mi0xLjYxQzQuNDIyIDE4LjA3IDMuNjMzIDE3LjcgMy42MzMgMTcuN2MtMS4wODctLjc0NC4wODQtLjcyOS4wODQtLjcyOSAxLjIwNS4wODQgMS44MzggMS4yMzYgMS44MzggMS4yMzYgMS4wNyAxLjgzNSAyLjgwOSAxLjMwNSAzLjQ5NS45OTguMTA4LS43NzYuNDE3LTEuMzA1Ljc2LTEuNjA1LTIuNjY1LS4zLTUuNDY2LTEuMzMyLTUuNDY2LTUuOTMgMC0xLjMxLjQ2NS0yLjM4IDEuMjM1LTMuMjItLjEzNS0uMzAzLS41NC0xLjUyMy4xMDUtMy4xNzYgMCAwIDEuMDA1LS4zMjIgMy4zIDEuMjMuOTYtLjI2NyAxLjk4LS4zOTkgMy0uNDA1IDEuMDIuMDA2IDIuMDQuMTM4IDMgLjQwNSAyLjI4LTEuNTUyIDMuMjg1LTEuMjMgMy4yODUtMS4yMy42NDUgMS42NTMuMjQgMi44NzMuMTIgMy4xNzYuNzY1Ljg0IDEuMjMgMS45MSAxLjIzIDMuMjIgMCA0LjYxLTIuODA1IDUuNjI1LTUuNDc1IDUuOTIuNDIuMzYuODEgMS4wOTYuODEgMi4yMiAwIDEuNjA2LS4wMTUgMi44OTYtLjAxNSAzLjI4NiAwIC4zMTUuMjEuNjkuODI1LjU3QzIwLjU2NSAyMi4wOTIgMjQgMTcuNTkyIDI0IDEyLjI5N2MwLTYuNjI3LTUuMzczLTEyLTEyLTEyIi8+PC9zdmc+', ) + // use simple icon with auto logo size + given({ name: 'amd', size: 'auto' }).expect( + 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCA1LjcyNTk5OTk5OTk5OTk5OSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+QU1EPC90aXRsZT48cGF0aCBkPSJNMTguMzI0IDBMMTkuODgzIDEuNTZIMjIuNDM5VjQuMTE3TDI0IDUuNjc3VjBaTTIgMC4zODNMMCA1LjM0M0gxLjMwOUwxLjY3OSA0LjM2MUgzLjlMNC4zMDggNS4zNDNINS42NDZMMy40MzIgMC4zODNaTTYuMjA5IDAuMzgzVjUuMzM4SDcuNDQ3VjIuMjQ2TDguNzg1IDMuODA4SDguOTczTDEwLjMxMSAyLjI1MlY1LjM0M0gxMS41NDlWMC4zODNIMTAuNDdMOC44NzggMi4yMjhMNy4yODcgMC4zODNaTTEyLjQ5MiAwLjM4M1Y1LjM0M0gxNC41NDlDMTYuNTI4IDUuMzQzIDE3LjQyOSA0LjI5NyAxNy40MjkgMi44NzFDMTcuNDI5IDEuNTExIDE2LjQ5MiAwLjM4MyAxNC42ODIgMC4zODNaTTEzLjcyOSAxLjI5M0gxNC41MjFDMTUuNjkxIDEuMjkzIDE2LjE1MSAyLjAwNCAxNi4xNTEgMi44NjNDMTYuMTUxIDMuNTkxIDE1Ljc3OSA0LjQzNSAxNC41MzUgNC40MzVIMTMuNzI5Wk0yLjc0NCAxLjU2NkwzLjUzNSAzLjQ5OEgyLjAwOFpNMTkuODgxIDEuODczTDE4LjI3NyAzLjQ3NlY1LjcyNkgyMC41MjNMMjIuMTI3IDQuMTE5SDE5Ljg4MVoiLz48L3N2Zz4=', + ) + // use simple icon with color & auto logo size + given({ name: 'amd', color: 'white', size: 'auto' }).expect( + 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgMjQgNS43MjU5OTk5OTk5OTk5OTkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkFNRDwvdGl0bGU+PHBhdGggZD0iTTE4LjMyNCAwTDE5Ljg4MyAxLjU2SDIyLjQzOVY0LjExN0wyNCA1LjY3N1YwWk0yIDAuMzgzTDAgNS4zNDNIMS4zMDlMMS42NzkgNC4zNjFIMy45TDQuMzA4IDUuMzQzSDUuNjQ2TDMuNDMyIDAuMzgzWk02LjIwOSAwLjM4M1Y1LjMzOEg3LjQ0N1YyLjI0Nkw4Ljc4NSAzLjgwOEg4Ljk3M0wxMC4zMTEgMi4yNTJWNS4zNDNIMTEuNTQ5VjAuMzgzSDEwLjQ3TDguODc4IDIuMjI4TDcuMjg3IDAuMzgzWk0xMi40OTIgMC4zODNWNS4zNDNIMTQuNTQ5QzE2LjUyOCA1LjM0MyAxNy40MjkgNC4yOTcgMTcuNDI5IDIuODcxQzE3LjQyOSAxLjUxMSAxNi40OTIgMC4zODMgMTQuNjgyIDAuMzgzWk0xMy43MjkgMS4yOTNIMTQuNTIxQzE1LjY5MSAxLjI5MyAxNi4xNTEgMi4wMDQgMTYuMTUxIDIuODYzQzE2LjE1MSAzLjU5MSAxNS43NzkgNC40MzUgMTQuNTM1IDQuNDM1SDEzLjcyOVpNMi43NDQgMS41NjZMMy41MzUgMy40OThIMi4wMDhaTTE5Ljg4MSAxLjg3M0wxOC4yNzcgMy40NzZWNS43MjZIMjAuNTIzTDIyLjEyNyA0LjExOUgxOS44ODFaIi8+PC9zdmc+', + ) // use travis shield icon given({ name: 'travis' }).expect( 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 bb58d66183d03..3151ae311d6e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "semver": "~7.6.0", "simple-icons": "11.10.0", "smol-toml": "1.1.4", + "svg-path-commander": "^2.0.9", "webextension-store-meta": "^1.1.0", "xpath": "~0.0.34" }, @@ -5081,6 +5082,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", @@ -27483,6 +27489,18 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "node_modules/svg-path-commander": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/svg-path-commander/-/svg-path-commander-2.0.9.tgz", + "integrity": "sha512-VfRLznHewlpQvuahtBK0MT/PlWAapbTx8RSytqgaVwD3US2keKcc3WYYlBBk4vIOR+jB3nQu/NAVlWHKlo0Fjw==", + "dependencies": { + "@thednp/dommatrix": "^2.0.6" + }, + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, "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 b19362b0e51a0..ae9847c522015 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "semver": "~7.6.0", "simple-icons": "11.10.0", "smol-toml": "1.1.4", + "svg-path-commander": "^2.0.9", "webextension-store-meta": "^1.1.0", "xpath": "~0.0.34" },