From c983a1320fa60ab8198bdca72ac31b26432a3bfe Mon Sep 17 00:00:00 2001 From: andyexeter Date: Fri, 6 Mar 2026 11:10:33 +0000 Subject: [PATCH 1/2] Make liquidSingleQuote work for Twig specific code --- src/printer/print/liquid.ts | 43 +++++++++++++------------ src/printer/utils/string.ts | 37 +++++++++++++++++++++ test/twig-function-quotes/fixed.liquid | 21 ++++++++++++ test/twig-function-quotes/index.liquid | 21 ++++++++++++ test/twig-function-quotes/index.spec.ts | 6 ++++ 5 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 test/twig-function-quotes/fixed.liquid create mode 100644 test/twig-function-quotes/index.liquid create mode 100644 test/twig-function-quotes/index.spec.ts diff --git a/src/printer/print/liquid.ts b/src/printer/print/liquid.ts index 84daa756..d097d561 100644 --- a/src/printer/print/liquid.ts +++ b/src/printer/print/liquid.ts @@ -34,6 +34,7 @@ import { shouldPreserveContent, FORCE_FLAT_GROUP_ID, last, + transformStringQuotes, } from '~/printer/utils'; import { printChildren } from '~/printer/print/children'; @@ -47,7 +48,7 @@ const { replaceEndOfLine } = doc.utils as any; export function printLiquidDrop( path: LiquidAstPath, - _options: LiquidParserOptions, + options: LiquidParserOptions, print: LiquidPrinter, { leadingSpaceGroupId, trailingSpaceGroupId }: LiquidPrinterArgs, ) { @@ -75,8 +76,11 @@ export function printLiquidDrop( ]); } + // Transform quotes in base case markup based on liquidSingleQuote option + const markup = transformStringQuotes(node.markup, options.liquidSingleQuote); + // This should probably be better than this but it'll do for now. - const lines = markupLines(node.markup); + const lines = markupLines(markup); if (lines.length > 1) { return group([ '{{', @@ -88,15 +92,7 @@ export function printLiquidDrop( ]); } - return group([ - '{{', - whitespaceStart, - ' ', - node.markup, - ' ', - whitespaceEnd, - '}}', - ]); + return group(['{{', whitespaceStart, ' ', markup, ' ', whitespaceEnd, '}}']); } function printNamedLiquidBlockStart( @@ -267,18 +263,22 @@ function printNamedLiquidBlockStart( function printLiquidStatement( path: AstPath>, - _options: LiquidParserOptions, + options: LiquidParserOptions, _print: LiquidPrinter, _args: LiquidPrinterArgs, ): Doc { const node = path.getValue(); + const transformedMarkup = transformStringQuotes( + node.markup, + options.liquidSingleQuote, + ); const shouldSkipLeadingSpace = - node.markup.trim() === '' || - (node.name === '#' && node.markup.startsWith('#')); + transformedMarkup.trim() === '' || + (node.name === '#' && transformedMarkup.startsWith('#')); return doc.utils.removeLines([ node.name, shouldSkipLeadingSpace ? '' : ' ', - node.markup, + transformedMarkup, ]); } @@ -324,7 +324,12 @@ export function printLiquidBlockStart( ); } - const lines = markupLines(node.markup); + // Transform quotes in base case markup based on liquidSingleQuote option + const transformedMarkup = transformStringQuotes( + node.markup, + options.liquidSingleQuote, + ); + const lines = markupLines(transformedMarkup); if (node.name === 'liquid') { return group([ @@ -351,11 +356,10 @@ export function printLiquidBlockStart( ]); } - const markup = node.markup; return group([ '{#', whitespaceStart, - markup ? ` ${markup}` : '', + transformedMarkup ? ` ${transformedMarkup}` : '', ' ', whitespaceEnd, '#}', @@ -373,13 +377,12 @@ export function printLiquidBlockStart( ]); } - const markup = node.markup; return group([ '{%', whitespaceStart, ' ', node.name, - markup ? ` ${markup}` : '', + transformedMarkup ? ` ${transformedMarkup}` : '', ' ', whitespaceEnd, '%}', diff --git a/src/printer/utils/string.ts b/src/printer/utils/string.ts index 731b8ded..b0db58c1 100644 --- a/src/printer/utils/string.ts +++ b/src/printer/utils/string.ts @@ -65,3 +65,40 @@ export function hasMoreThanOneNewLineBetweenNodes( const count = between.match(/\n/g)?.length || 0; return count > 1; } + +/** + * Transforms quotes in base case markup strings based on the liquidSingleQuote option. + * This handles cases where the parser falls back to storing markup as a raw string + * (e.g., Twig function calls like `stimulus_controller('controller-name')`). + * + * The function replaces quotes while being careful to: + * - Not replace quotes that are escaped + * - Not replace quotes inside strings that contain the target quote character + * - Handle nested quotes properly + */ +export function transformStringQuotes( + markup: string, + liquidSingleQuote: boolean, +): string { + const preferredQuote = liquidSingleQuote ? "'" : '"'; + + // Match strings with the non-preferred quote style + // This regex matches quoted strings, being careful about escapes + const stringRegex = liquidSingleQuote + ? /"([^"\\]|\\.)*"/g // Match double-quoted strings + : /'([^'\\]|\\.)*'/g; // Match single-quoted strings + + return markup.replace(stringRegex, (match) => { + // Get the content without the outer quotes + const content = match.slice(1, -1); + + // If the content contains the preferred quote (unescaped), keep original quotes + // to avoid breaking the string + if (content.includes(preferredQuote)) { + return match; + } + + // Replace the quotes + return preferredQuote + content + preferredQuote; + }); +} diff --git a/test/twig-function-quotes/fixed.liquid b/test/twig-function-quotes/fixed.liquid new file mode 100644 index 00000000..cddde138 --- /dev/null +++ b/test/twig-function-quotes/fixed.liquid @@ -0,0 +1,21 @@ +It should convert double quotes to single quotes in Twig function calls (base case) +{{ stimulus_controller('controller-name') }} +
+ +It should keep double quotes if the string contains single quotes +{{ stimulus_controller("it's a name") }} + +It should convert double quotes to single quotes in tag base case +{% set foo = 'bar' %} + +It should handle multiple arguments with quotes +{{ some_function('arg1', 'arg2') }} + +It should convert to double quotes when liquidSingleQuote is false +liquidSingleQuote: false +{{ stimulus_controller("controller-name") }} + +It should keep single quotes if string contains double quotes when liquidSingleQuote is false +liquidSingleQuote: false +{{ stimulus_controller('say "hello"') }} + diff --git a/test/twig-function-quotes/index.liquid b/test/twig-function-quotes/index.liquid new file mode 100644 index 00000000..593df684 --- /dev/null +++ b/test/twig-function-quotes/index.liquid @@ -0,0 +1,21 @@ +It should convert double quotes to single quotes in Twig function calls (base case) +{{ stimulus_controller("controller-name") }} +
+ +It should keep double quotes if the string contains single quotes +{{ stimulus_controller("it's a name") }} + +It should convert double quotes to single quotes in tag base case +{% set foo = "bar" %} + +It should handle multiple arguments with quotes +{{ some_function("arg1", "arg2") }} + +It should convert to double quotes when liquidSingleQuote is false +liquidSingleQuote: false +{{ stimulus_controller('controller-name') }} + +It should keep single quotes if string contains double quotes when liquidSingleQuote is false +liquidSingleQuote: false +{{ stimulus_controller('say "hello"') }} + diff --git a/test/twig-function-quotes/index.spec.ts b/test/twig-function-quotes/index.spec.ts new file mode 100644 index 00000000..4587999a --- /dev/null +++ b/test/twig-function-quotes/index.spec.ts @@ -0,0 +1,6 @@ +import { assertFormattedEqualsFixed } from '../test-helpers'; +import * as path from 'path'; + +describe(`Unit: ${path.basename(__dirname)}`, () => { + assertFormattedEqualsFixed(__dirname); +}); From 95375cfd1f3cdd4247d802470d1524f450f18025 Mon Sep 17 00:00:00 2001 From: andyexeter Date: Fri, 6 Mar 2026 13:34:31 +0000 Subject: [PATCH 2/2] Ensure HTML attribute quotes within Twig comments aren't changed --- src/printer/print/liquid.ts | 44 ++++++++++++++------------ test/twig-function-quotes/fixed.liquid | 2 ++ test/twig-function-quotes/index.liquid | 2 ++ 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/printer/print/liquid.ts b/src/printer/print/liquid.ts index d097d561..0f359dc9 100644 --- a/src/printer/print/liquid.ts +++ b/src/printer/print/liquid.ts @@ -324,27 +324,9 @@ export function printLiquidBlockStart( ); } - // Transform quotes in base case markup based on liquidSingleQuote option - const transformedMarkup = transformStringQuotes( - node.markup, - options.liquidSingleQuote, - ); - const lines = markupLines(transformedMarkup); - - if (node.name === 'liquid') { - return group([ - '{%', - whitespaceStart, - ' ', - node.name, - indent([hardline, join(hardline, reindent(lines, true))]), - hardline, - whitespaceEnd, - '%}', - ]); - } - + // For Twig comments ({# ... #}), don't transform quotes - comments should be preserved as-is if (node.name === 'twig') { + const lines = markupLines(node.markup); if (lines.length > 1) { return group([ '{#', @@ -359,13 +341,33 @@ export function printLiquidBlockStart( return group([ '{#', whitespaceStart, - transformedMarkup ? ` ${transformedMarkup}` : '', + node.markup ? ` ${node.markup}` : '', ' ', whitespaceEnd, '#}', ]); } + // Transform quotes in base case markup based on liquidSingleQuote option + const transformedMarkup = transformStringQuotes( + node.markup, + options.liquidSingleQuote, + ); + const lines = markupLines(transformedMarkup); + + if (node.name === 'liquid') { + return group([ + '{%', + whitespaceStart, + ' ', + node.name, + indent([hardline, join(hardline, reindent(lines, true))]), + hardline, + whitespaceEnd, + '%}', + ]); + } + if (lines.length > 1) { return group([ '{%', diff --git a/test/twig-function-quotes/fixed.liquid b/test/twig-function-quotes/fixed.liquid index cddde138..ec244957 100644 --- a/test/twig-function-quotes/fixed.liquid +++ b/test/twig-function-quotes/fixed.liquid @@ -19,3 +19,5 @@ It should keep single quotes if string contains double quotes when liquidSingleQ liquidSingleQuote: false {{ stimulus_controller('say "hello"') }} +It should not modify HTML attributes within Twig comments +{#
#} diff --git a/test/twig-function-quotes/index.liquid b/test/twig-function-quotes/index.liquid index 593df684..0972ccd2 100644 --- a/test/twig-function-quotes/index.liquid +++ b/test/twig-function-quotes/index.liquid @@ -19,3 +19,5 @@ It should keep single quotes if string contains double quotes when liquidSingleQ liquidSingleQuote: false {{ stimulus_controller('say "hello"') }} +It should not modify HTML attributes within Twig comments +{#
#}