From 0ce04d6f2cb4eb7ebf20dc8aa4624ebe9bb919b9 Mon Sep 17 00:00:00 2001 From: valkyrie_pilot Date: Sun, 13 Oct 2024 10:32:40 -0600 Subject: [PATCH] Add option for IDs to have a prefix (#10576) * Add option for IDs to have a prefix * Switch to suffixes to protect ID values * Document idSuffix * Fix validation error message consistency * Fix ID suffix regex test * Add tests * snapshot tests for idSuffix covering all 5 badge styles * tweak docs * update typescript definitions * badge-maker 4.1.0 release --------- Co-authored-by: chris48s --- __snapshots__/make-badge.spec.js | 322 +++++++++++++++++++++++++++++ badge-maker/CHANGELOG.md | 6 + badge-maker/README.md | 5 + badge-maker/index.d.ts | 1 + badge-maker/lib/badge-renderers.js | 27 +-- badge-maker/lib/index.js | 7 + badge-maker/lib/index.spec.js | 6 + badge-maker/lib/make-badge.js | 3 + badge-maker/lib/make-badge.spec.js | 64 ++++++ badge-maker/package.json | 2 +- package-lock.json | 2 +- 11 files changed, 431 insertions(+), 14 deletions(-) diff --git a/__snapshots__/make-badge.spec.js b/__snapshots__/make-badge.spec.js index df826a8ca14a3..3b149e98505f4 100644 --- a/__snapshots__/make-badge.spec.js +++ b/__snapshots__/make-badge.spec.js @@ -2229,3 +2229,325 @@ exports['The badge generator badges with logos should always produce the same ba ` + +exports['The badge generator "flat" template badge generation should match snapshots: message with custom suffix 1'] = ` + + cactus: grown + + + + + + + + + + + + + + + + + cactus + + + + grown + + + + +` + +exports['The badge generator "flat-square" template badge generation should match snapshots: message with custom suffix 1'] = ` + + cactus: grown + + + + + + + + cactus + + + grown + + + + +` + +exports['The badge generator "plastic" template badge generation should match snapshots: message with custom suffix 1'] = ` + + cactus: grown + + + + + + + + + + + + + + + + + + + cactus + + + + grown + + + + +` + +exports['The badge generator "for-the-badge" template badge generation should match snapshots: message with custom suffix 1'] = ` + + CACTUS: GROWN + + + + + + + + CACTUS + + + GROWN + + + + +` + +exports['The badge generator "social" template badge generation should match snapshots: message with custom suffix 1'] = ` + + Cactus: grown + + + + + + + + + + + + + + + + + + + +` diff --git a/badge-maker/CHANGELOG.md b/badge-maker/CHANGELOG.md index ad347fc3a7916..4cf5a079ee468 100644 --- a/badge-maker/CHANGELOG.md +++ b/badge-maker/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.1.0 + +### Features + +- Add `idSuffix` param. This can be used to ensure every element id within the SVG is unique + ## 4.0.0 ### Breaking Changes diff --git a/badge-maker/README.md b/badge-maker/README.md index 5a8c50631ebb0..3bb10503cc8f7 100644 --- a/badge-maker/README.md +++ b/badge-maker/README.md @@ -73,6 +73,11 @@ The format is the following: // (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social' // Each offers a different visual design. style: 'flat', + + // (Optional) A string with only letters, numbers, -, and _. This can be used + // to ensure every element id within the SVG is unique and prevent CSS + // cross-contamination when the SVG badge is rendered inline in HTML pages. + idSuffix: 'dd' } ``` diff --git a/badge-maker/index.d.ts b/badge-maker/index.d.ts index 5a1b6872ebe81..96ad5332a770c 100644 --- a/badge-maker/index.d.ts +++ b/badge-maker/index.d.ts @@ -6,6 +6,7 @@ interface Format { style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social' logoBase64?: string links?: Array + idSuffix?: string } export declare class ValidationError extends Error {} diff --git a/badge-maker/lib/badge-renderers.js b/badge-maker/lib/badge-renderers.js index af726101e0819..a20201b863430 100644 --- a/badge-maker/lib/badge-renderers.js +++ b/badge-maker/lib/badge-renderers.js @@ -128,6 +128,7 @@ class Badge { logoPadding, color = '#4c1', labelColor, + idSuffix = '', }) { const horizPadding = 5 const hasLogo = !!logo @@ -178,6 +179,7 @@ class Badge { this.label = label this.message = message this.accessibleText = accessibleText + this.idSuffix = idSuffix this.logoElement = getLogoElement({ logo, @@ -286,7 +288,7 @@ class Badge { }, }), ], - attrs: { id: 'r' }, + attrs: { id: `r${this.idSuffix}` }, }) } @@ -313,7 +315,7 @@ class Badge { attrs: { width: this.width, height: this.constructor.height, - fill: 'url(#s)', + fill: `url(#s${this.idSuffix})`, }, }) const content = withGradient @@ -379,14 +381,14 @@ class Plastic extends Badge { attrs: { offset: 1, 'stop-color': '#000', 'stop-opacity': '.5' }, }), ], - attrs: { id: 's', x2: 0, y2: '100%' }, + attrs: { id: `s${this.idSuffix}`, x2: 0, y2: '100%' }, }) const clipPath = this.getClipPathElement(4) const backgroundGroup = this.getBackgroundGroupElement({ withGradient: true, - attrs: { 'clip-path': 'url(#r)' }, + attrs: { 'clip-path': `url(#r${this.idSuffix})` }, }) return renderBadge( @@ -428,14 +430,14 @@ class Flat extends Badge { attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 's', x2: 0, y2: '100%' }, + attrs: { id: `s${this.idSuffix}`, x2: 0, y2: '100%' }, }) const clipPath = this.getClipPathElement(3) const backgroundGroup = this.getBackgroundGroupElement({ withGradient: true, - attrs: { 'clip-path': 'url(#r)' }, + attrs: { 'clip-path': `url(#r${this.idSuffix})` }, }) return renderBadge( @@ -492,6 +494,7 @@ function social({ logoPadding, color = '#4c1', labelColor = '#555', + idSuffix = '', }) { // Social label is styled with a leading capital. Convert to caps here so // width can be measured using the correct characters. @@ -565,9 +568,9 @@ function social({ const rect = new XmlElement({ name: 'rect', attrs: { - id: 'llink', + id: `llink${idSuffix}`, stroke: '#d5d5d5', - fill: 'url(#a)', + fill: `url(#a${idSuffix})`, x: '.5', y: '.5', width: labelRectWidth, @@ -640,7 +643,7 @@ function social({ name: 'text', content: [message], attrs: { - id: 'rlink', + id: `rlink${idSuffix}`, x: messageTextX, y: 140, transform: FONT_SCALE_DOWN_VALUE, @@ -660,7 +663,7 @@ function social({ const style = new XmlElement({ name: 'style', content: [ - 'a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{fill:#4183c4}', + `a:hover #llink${idSuffix}{fill:url(#b${idSuffix});stroke:#ccc}a:hover #rlink${idSuffix}{fill:#4183c4}`, ], }) const gradients = new ElementList({ @@ -681,7 +684,7 @@ function social({ attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 'a', x2: 0, y2: '100%' }, + attrs: { id: `a${idSuffix}`, x2: 0, y2: '100%' }, }), new XmlElement({ name: 'linearGradient', @@ -695,7 +698,7 @@ function social({ attrs: { offset: 1, 'stop-opacity': '.1' }, }), ], - attrs: { id: 'b', x2: 0, y2: '100%' }, + attrs: { id: `b${idSuffix}`, x2: 0, y2: '100%' }, }), ], }) diff --git a/badge-maker/lib/index.js b/badge-maker/lib/index.js index fa9caec94bf75..881188f411051 100644 --- a/badge-maker/lib/index.js +++ b/badge-maker/lib/index.js @@ -52,6 +52,11 @@ function _validate(format) { `Field \`style\` must be one of (${styleValues.toString()})`, ) } + if ('idSuffix' in format && !/^[a-zA-Z0-9\-_]*$/.test(format.idSuffix)) { + throw new ValidationError( + 'Field `idSuffix` must contain only numbers, letters, -, and _', + ) + } } function _clean(format) { @@ -63,6 +68,7 @@ function _clean(format) { 'style', 'logoBase64', 'links', + 'idSuffix', ] const cleaned = {} @@ -95,6 +101,7 @@ function _clean(format) { * @param {string} format.style (Optional) Visual style (e.g: 'flat') * @param {string} format.logoBase64 (Optional) Logo data URL * @param {Array} format.links (Optional) Links array (e.g: ['https://example.com', 'https://example.com']) + * @param {string} format.idSuffix (Optional) Suffix for IDs, e.g. 1, 2, and 3 for three invocations that will be used on the same page. * @returns {string} Badge in SVG format * @see https://github.com/badges/shields/tree/master/badge-maker/README.md */ diff --git a/badge-maker/lib/index.spec.js b/badge-maker/lib/index.spec.js index 5e63453c6c0e9..d19185570de28 100644 --- a/badge-maker/lib/index.spec.js +++ b/badge-maker/lib/index.spec.js @@ -101,5 +101,11 @@ describe('makeBadge function', function () { ValidationError, 'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)', ) + expect(() => + makeBadge({ label: 'build', message: 'passed', idSuffix: '\\' }), + ).to.throw( + ValidationError, + 'Field `idSuffix` must contain only numbers, letters, -, and _', + ) }) }) diff --git a/badge-maker/lib/make-badge.js b/badge-maker/lib/make-badge.js index 0556427f8f21f..7119670d47ce0 100644 --- a/badge-maker/lib/make-badge.js +++ b/badge-maker/lib/make-badge.js @@ -20,6 +20,7 @@ module.exports = function makeBadge({ logoSize, logoWidth, links = ['', ''], + idSuffix, }) { // String coercion and whitespace removal. label = `${label}`.trim() @@ -38,6 +39,7 @@ module.exports = function makeBadge({ link: links, name: label, value: message, + idSuffix, }) } @@ -59,6 +61,7 @@ module.exports = function makeBadge({ logoPadding: logo && label.length ? 3 : 0, color: toSvgColor(color), labelColor: toSvgColor(labelColor), + idSuffix, }), ) } diff --git a/badge-maker/lib/make-badge.spec.js b/badge-maker/lib/make-badge.spec.js index 9170a3d37bc70..da28fcbf8520a 100644 --- a/badge-maker/lib/make-badge.spec.js +++ b/badge-maker/lib/make-badge.spec.js @@ -167,6 +167,18 @@ describe('The badge generator', function () { }) }) + it('should match snapshots: message with custom suffix', async function () { + await expectBadgeToMatchSnapshot({ + label: 'cactus', + message: 'grown', + format: 'svg', + color: '#b3e', + labelColor: '#0f0', + logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', + idSuffix: '1', + }) + }) + it('should match snapshots: message only, no logo', async function () { await expectBadgeToMatchSnapshot({ label: '', @@ -259,6 +271,19 @@ describe('The badge generator', function () { }) }) + it('should match snapshots: message with custom suffix', async function () { + await expectBadgeToMatchSnapshot({ + label: 'cactus', + message: 'grown', + format: 'svg', + style: 'flat-square', + color: '#b3e', + labelColor: '#0f0', + logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', + idSuffix: '1', + }) + }) + it('should match snapshots: message only, no logo', async function () { await expectBadgeToMatchSnapshot({ label: '', @@ -351,6 +376,19 @@ describe('The badge generator', function () { }) }) + it('should match snapshots: message with custom suffix', async function () { + await expectBadgeToMatchSnapshot({ + label: 'cactus', + message: 'grown', + format: 'svg', + style: 'plastic', + color: '#b3e', + labelColor: '#0f0', + logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', + idSuffix: '1', + }) + }) + it('should match snapshots: message only, no logo', async function () { await expectBadgeToMatchSnapshot({ label: '', @@ -470,6 +508,19 @@ describe('The badge generator', function () { }) }) + it('should match snapshots: message with custom suffix', async function () { + await expectBadgeToMatchSnapshot({ + label: 'cactus', + message: 'grown', + format: 'svg', + style: 'for-the-badge', + color: '#b3e', + labelColor: '#0f0', + logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', + idSuffix: '1', + }) + }) + it('should match snapshots: message only, no logo', async function () { await expectBadgeToMatchSnapshot({ label: '', @@ -589,6 +640,19 @@ describe('The badge generator', function () { }) }) + it('should match snapshots: message with custom suffix', async function () { + await expectBadgeToMatchSnapshot({ + label: 'cactus', + message: 'grown', + format: 'svg', + style: 'social', + color: '#b3e', + labelColor: '#0f0', + logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', + idSuffix: '1', + }) + }) + it('should match snapshots: message only, no logo', async function () { await expectBadgeToMatchSnapshot({ label: '', diff --git a/badge-maker/package.json b/badge-maker/package.json index 56ec5bfd7249f..1401d836ff443 100644 --- a/badge-maker/package.json +++ b/badge-maker/package.json @@ -1,6 +1,6 @@ { "name": "badge-maker", - "version": "4.0.0", + "version": "4.1.0", "description": "Shields.io badge library", "keywords": [ "GitHub", diff --git a/package-lock.json b/package-lock.json index 6a9db75bef243..ab713135cbcd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,7 +134,7 @@ } }, "badge-maker": { - "version": "4.0.0", + "version": "4.1.0", "license": "CC0-1.0", "dependencies": { "anafanafo": "2.0.0",