diff --git a/services/dash-badge-content-helpers.js b/services/dash-badge-content-helpers.js new file mode 100644 index 0000000000000..c21c669f5c11c --- /dev/null +++ b/services/dash-badge-content-helpers.js @@ -0,0 +1,39 @@ +/** + * Helpers for parsing badge content from a dash seperated single parameter. + * + * @module + */ + +/** + * Split string on dashses, but escape dash if double. + * Escape slash if double. + * + * @param {string} s - String to split. + * @returns {string[]} Parts of the string, split on unescaped dashes. + */ +function splitDashSeparatedOptionalParams(s) { + const parts = [] + let cur = '' + for (let i = 0; i < s.length; ) { + const ch = s[i] + const next = s[i + 1] + if (ch === '-' && next === '-') { + cur += '-' + i += 2 + } else if (ch === '/' && next === '/') { + cur += '/' + i += 2 + } else if (ch === '-') { + parts.push(cur) + cur = '' + i += 1 + } else { + cur += ch + i += 1 + } + } + parts.push(cur) + return parts +} + +export { splitDashSeparatedOptionalParams } diff --git a/services/dash-badge-content-helpers.spec.js b/services/dash-badge-content-helpers.spec.js new file mode 100644 index 0000000000000..670f3053d429c --- /dev/null +++ b/services/dash-badge-content-helpers.spec.js @@ -0,0 +1,13 @@ +import { test, given } from 'sazerac' +import { splitDashSeparatedOptionalParams } from './dash-badge-content-helpers.js' + +describe('Dash badge content helpers', function () { + test(splitDashSeparatedOptionalParams, () => { + given('foo-bar-baz').expect(['foo', 'bar', 'baz']) + given('foo--bar-baz').expect(['foo-bar', 'baz']) + given('foo-bar--baz').expect(['foo', 'bar-baz']) + given('foo--bar--baz').expect(['foo-bar-baz']) + given('foo//bar-baz').expect(['foo/bar', 'baz']) + given('foo//bar//baz').expect(['foo/bar/baz']) + }) +}) diff --git a/services/static-badge/static-badge-colon-redirect.service.js b/services/static-badge/static-badge-colon-redirect.service.js new file mode 100644 index 0000000000000..a56938de554e8 --- /dev/null +++ b/services/static-badge/static-badge-colon-redirect.service.js @@ -0,0 +1,12 @@ +import { redirector } from '../index.js' + +export default redirector({ + category: 'static', + name: 'StaticBadgeColonRedirect', + route: { + base: '', + pattern: '\\::badgeContent', + }, + transformPath: ({ badgeContent }) => `/badge/${badgeContent}`, + dateAdded: new Date('2026-05-21'), +}) diff --git a/services/static-badge/static-badge-colon-redirect.tester.js b/services/static-badge/static-badge-colon-redirect.tester.js new file mode 100644 index 0000000000000..bfe598e10acb1 --- /dev/null +++ b/services/static-badge/static-badge-colon-redirect.tester.js @@ -0,0 +1,22 @@ +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('static badge colon redirect: basic redirect') + .get('/:label-message-blue') + .expectRedirect('/badge/label-message-blue.svg') + +t.create('static badge colon redirect: with spaces and dash encoding') + .get('/:all%20one%20color-red') + .expectRedirect('/badge/all%20one%20color-red.svg') + +t.create('static badge colon redirect: double dash and underscore encoding') + .get('/:best--license-Apache--2.0-blue') + .expectRedirect('/badge/best--license-Apache--2.0-blue.svg') + +t.create('static badge colon redirect: missing label') + .get('/:-message-blue') + .expectRedirect('/badge/-message-blue.svg') + +t.create('static badge colon redirect: missing message') + .get('/:label--blue') + .expectRedirect('/badge/label--blue.svg') diff --git a/services/static-badge/static-badge.service.js b/services/static-badge/static-badge.service.js index 910a79f962b8d..f40748d7cd307 100644 --- a/services/static-badge/static-badge.service.js +++ b/services/static-badge/static-badge.service.js @@ -1,5 +1,7 @@ import { escapeFormat } from '../../core/badge-urls/path-helpers.js' +import { InvalidParameter } from '../../core/base-service/errors.js' import { BaseStaticService } from '../index.js' +import { splitDashSeparatedOptionalParams } from '../dash-badge-content-helpers.js' const description = ` The static badge accepts a single required path parameter which encodes either: @@ -61,8 +63,7 @@ export default class StaticBadge extends BaseStaticService { static category = 'static' static route = { base: '', - format: '(?::|badge/)((?:[^-]|--)*?)-?((?:[^-]|--)*)-((?:[^-.]|--)+)', - capture: ['label', 'message', 'color'], + pattern: 'badge/:badgeContent', } static openApi = { @@ -85,7 +86,26 @@ export default class StaticBadge extends BaseStaticService { }, } - handle({ label, message, color }) { - return { label: escapeFormat(label), message: escapeFormat(message), color } + handle({ badgeContent }) { + const parts = splitDashSeparatedOptionalParams(badgeContent) + switch (parts.length) { + case 2: { + const [message, color] = parts + return { label: '', message: escapeFormat(message), color } + } + case 3: { + const [label, message, color] = parts + return { + label: escapeFormat(label), + message: escapeFormat(message), + color, + } + } + default: + throw new InvalidParameter({ + prettyMessage: + 'badgeContent must have either 2 or 3 dash-separated parts', + }) + } } } diff --git a/services/static-badge/static-badge.tester.js b/services/static-badge/static-badge.tester.js index dc0b0218c0348..432def258e0ac 100644 --- a/services/static-badge/static-badge.tester.js +++ b/services/static-badge/static-badge.tester.js @@ -31,7 +31,7 @@ t.create('Not a valid color') }) t.create('Missing message') - .get('/badge/label--blue.json') + .get('/badge/label-%20-blue.json') .expectBadge({ label: 'label', message: '', color: 'blue' }) t.create('Missing label') diff --git a/services/website/website-redirect.service.js b/services/website/website-redirect.service.js index e1136d5713f9b..f8f0709994d1a 100644 --- a/services/website/website-redirect.service.js +++ b/services/website/website-redirect.service.js @@ -1,5 +1,6 @@ import { escapeFormat } from '../../core/badge-urls/path-helpers.js' import { redirector } from '../index.js' +import { splitDashSeparatedOptionalParams } from '../dash-badge-content-helpers.js' function escapeFormatSlashes(t) { return ( @@ -9,31 +10,6 @@ function escapeFormatSlashes(t) { ) } -function splitDashSeparatedOptionalParams(s) { - const parts = [] - let cur = '' - for (let i = 0; i < s.length; ) { - const ch = s[i] - const next = s[i + 1] - if (ch === '-' && next === '-') { - cur += '-' - i += 2 - } else if (ch === '/' && next === '/') { - cur += '/' - i += 2 - } else if (ch === '-') { - parts.push(cur) - cur = '' - i += 1 - } else { - cur += ch - i += 1 - } - } - parts.push(cur) - return parts -} - /* Old documentation, for reference: