From 5ac881901cfb3f3a7e2088bc95c33968abe37c31 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Mon, 20 Jul 2026 03:39:13 -0400 Subject: [PATCH] fix(url_decode): keep %2B as a literal plus when decoding url_decode decoded the percent-encoding first and only then replaced "+" with a space, so a "%2B" became "+" and was immediately turned into a space. Any literal "+" was therefore lost when round-tripped through url_encode. I now replace "+" with a space before decodeURIComponent, which lines up with Ruby's CGI.unescape used by Shopify. --- src/filters/url.ts | 2 +- test/integration/filters/url.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/filters/url.ts b/src/filters/url.ts index 77a9cab1c8..ddfbd88ff0 100644 --- a/src/filters/url.ts +++ b/src/filters/url.ts @@ -1,6 +1,6 @@ import { stringify } from '../util/underscore' -export const url_decode = (x: string) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ') +export const url_decode = (x: string) => decodeURIComponent(stringify(x).replace(/\+/g, ' ')) export const url_encode = (x: string) => encodeURIComponent(stringify(x)).replace(/%20/g, '+') export const cgi_escape = (x: string) => encodeURIComponent(stringify(x)) .replace(/%20/g, '+') diff --git a/test/integration/filters/url.spec.ts b/test/integration/filters/url.spec.ts index b18649a49d..eef829b86f 100644 --- a/test/integration/filters/url.spec.ts +++ b/test/integration/filters/url.spec.ts @@ -7,6 +7,14 @@ describe('filters/url', () => { const html = liquid.parseAndRenderSync('{{ "%27Stop%21%27+said+Fred" | url_decode }}') expect(html).toEqual("'Stop!' said Fred") }) + it('should decode %2B to a literal plus', () => { + const html = liquid.parseAndRenderSync('{{ "1%2B1" | url_decode }}') + expect(html).toEqual('1+1') + }) + it('should keep a literal plus when round-tripped through url_encode', () => { + const html = liquid.parseAndRenderSync('{{ "a+b c" | url_encode | url_decode }}') + expect(html).toEqual('a+b c') + }) }) describe('url_encode', () => {