From 374ed56908137cefcdb4e4e8afbf7ba7ba196bb4 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 15 May 2026 20:03:30 +0300 Subject: [PATCH 1/6] refactor: move splitDashSeparatedOptionalParams to a new helper module --- services/dash-badge-content-helpers.js | 39 ++++++++++++++++++++ services/website/website-redirect.service.js | 26 +------------ 2 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 services/dash-badge-content-helpers.js 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/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: From 717825cdc5c7a31ed91b3c2cc841620928d9d2e2 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 15 May 2026 20:03:45 +0300 Subject: [PATCH 2/6] test: add unit tests for splitDashSeparatedOptionalParams helper --- services/dash-badge-content-helpers.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 services/dash-badge-content-helpers.spec.js 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']) + }) +}) From 1346091722745cbee67d4a3e89b91fcc189ce91f Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Fri, 15 May 2026 20:06:24 +0300 Subject: [PATCH 3/6] refactor: update StaticBadge to use pattern route Part of #3329 --- services/static-badge/static-badge.service.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/static-badge/static-badge.service.js b/services/static-badge/static-badge.service.js index 910a79f962b8d..364619587f761 100644 --- a/services/static-badge/static-badge.service.js +++ b/services/static-badge/static-badge.service.js @@ -1,5 +1,6 @@ import { escapeFormat } from '../../core/badge-urls/path-helpers.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 +62,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 +85,9 @@ export default class StaticBadge extends BaseStaticService { }, } - handle({ label, message, color }) { + handle({ badgeContent }) { + const [label, message, color] = + splitDashSeparatedOptionalParams(badgeContent) return { label: escapeFormat(label), message: escapeFormat(message), color } } } From 5c165d71b2d5f855de19d40bd5395b9fba266732 Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 21 May 2026 14:59:34 +0000 Subject: [PATCH 4/6] add StaticBadgeColonRedirect service and corresponding tests --- .../static-badge-colon-redirect.service.js | 12 ++++++++++ .../static-badge-colon-redirect.tester.js | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 services/static-badge/static-badge-colon-redirect.service.js create mode 100644 services/static-badge/static-badge-colon-redirect.tester.js 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') From 4abfd7d7874a97d8eba7eb5569bc1563ffdafb9d Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 21 May 2026 15:25:04 +0000 Subject: [PATCH 5/6] add support to message-color only content and present error message for invalid content --- services/static-badge/static-badge.service.js | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/services/static-badge/static-badge.service.js b/services/static-badge/static-badge.service.js index 364619587f761..f40748d7cd307 100644 --- a/services/static-badge/static-badge.service.js +++ b/services/static-badge/static-badge.service.js @@ -1,4 +1,5 @@ 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' @@ -86,8 +87,25 @@ export default class StaticBadge extends BaseStaticService { } handle({ badgeContent }) { - const [label, message, color] = - splitDashSeparatedOptionalParams(badgeContent) - return { label: escapeFormat(label), message: escapeFormat(message), color } + 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', + }) + } } } From 7f916a90daf47d4f013812b4609a911edd9db1db Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Thu, 21 May 2026 15:27:04 +0000 Subject: [PATCH 6/6] update test to remove support for missing char message label--blue and label- -blue produce the exact same results. our docs indicate -- outputs - so the label--blue syntax is not consistenet with our docs for use in missing message, and is more confusing considering our logic here. --- services/static-badge/static-badge.tester.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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')