diff --git a/.changeset/quiet-lamps-quote.md b/.changeset/quiet-lamps-quote.md new file mode 100644 index 000000000..ceccb6eaa --- /dev/null +++ b/.changeset/quiet-lamps-quote.md @@ -0,0 +1,5 @@ +--- +'@shopify/prettier-plugin-liquid': patch +--- + +Respect `liquidSingleQuote` when formatting Liquid strings inside HTML attribute values. diff --git a/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts b/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts index 1eabbaa38..74b98e22e 100644 --- a/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts +++ b/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts @@ -4,9 +4,11 @@ import { LiquidDocExampleNode, LiquidDocParamNode, LiquidDocPromptNode, + LiquidString, NodeTypes, Position, RawMarkupKinds, + walk, } from '@shopify/liquid-html-parser'; import { doc, Doc } from 'prettier'; import type { Printer as Printer2 } from 'prettier'; @@ -107,33 +109,8 @@ function printAttribute isTextLikeNode(valueNode) && valueNode.value.includes(preferredQuote), @@ -153,6 +130,51 @@ function printAttribute, + options: LiquidParserOptions, +) { + let value = node.source.slice(node.attributePosition.start, node.attributePosition.end); + const replacements = new Map(); + + for (const valueNode of node.value) { + if (isTextLikeNode(valueNode)) continue; + + walk(valueNode, (child) => { + if (child.type !== NodeTypes.String || typeof (child as LiquidString).single !== 'boolean') { + return; + } + + const stringNode = child as LiquidString; + const start = stringNode.position.start - node.attributePosition.start; + const end = stringNode.position.end - node.attributePosition.start; + + if (start < 0 || end > value.length || end - start < 2) return; + + const rawString = value.slice(start, end); + const originalQuote = stringNode.single ? "'" : '"'; + + if (rawString[0] !== originalQuote || rawString[rawString.length - 1] !== originalQuote) { + return; + } + + replacements.set(start, { end, quote: getLiquidStringQuote(stringNode.value, options) }); + }); + } + + for (const [start, replacement] of [...replacements.entries()].sort(([a], [b]) => b - a)) { + const { end, quote } = replacement; + value = `${value.slice(0, start)}${quote}${value.slice(start + 1, end - 1)}${quote}${value.slice(end)}`; + } + + return value; +} + +function getLiquidStringQuote(value: string, options: LiquidParserOptions) { + const preferredQuote = options.liquidSingleQuote ? "'" : '"'; + return value.includes(preferredQuote) ? oppositeQuotes[preferredQuote] : preferredQuote; +} + function isYamlFrontMatter(node: TextNode) { return ( node.parentNode && @@ -580,9 +602,7 @@ function printNode( } case NodeTypes.String: { - const preferredQuote = options.liquidSingleQuote ? `'` : `"`; - const valueHasQuotes = node.value.includes(preferredQuote); - const quote = valueHasQuotes ? oppositeQuotes[preferredQuote] : preferredQuote; + const quote = getLiquidStringQuote(node.value, options); return [quote, node.value, quote]; } diff --git a/packages/prettier-plugin-liquid/src/test/html-attributes/fixed.liquid b/packages/prettier-plugin-liquid/src/test/html-attributes/fixed.liquid index 1dced10b4..7f39eea49 100644 --- a/packages/prettier-plugin-liquid/src/test/html-attributes/fixed.liquid +++ b/packages/prettier-plugin-liquid/src/test/html-attributes/fixed.liquid @@ -140,13 +140,13 @@ If singleQuote is false, and the attribute value contained double quotes, use si singleQuote: false -If singleQuote is true, and the attribute value has liquid tags that contain single quotes, don't care. +It should keep Liquid single quotes inside single-quoted HTML attributes singleQuote: true -If singleQuote is false, and the attribute value has liquid tags that contain double quotes, don't care. +It should respect liquidSingleQuote inside double-quoted HTML attributes singleQuote: false - + It should pretty print attribute names with Liquid in them singleQuote: true @@ -163,3 +163,20 @@ It should pretty unquoted liquid drop attributes It should pretty print smart quotes into dumb quotes to prevent copy pasting errors

+ +It should respect liquidSingleQuote inside HTML attribute values +liquidSingleQuote: true +
+ +It should format Liquid string quotes inside an attribute without reformatting other Liquid markup +liquidSingleQuote: true +
+ +It should preserve Liquid strings that contain the preferred quote +liquidSingleQuote: true +
+ +It should respect liquidSingleQuote false inside HTML attribute values +liquidSingleQuote: false +singleQuote: true +
diff --git a/packages/prettier-plugin-liquid/src/test/html-attributes/index.liquid b/packages/prettier-plugin-liquid/src/test/html-attributes/index.liquid index 404b14aee..c95a1d15c 100644 --- a/packages/prettier-plugin-liquid/src/test/html-attributes/index.liquid +++ b/packages/prettier-plugin-liquid/src/test/html-attributes/index.liquid @@ -86,11 +86,11 @@ If singleQuote is false, and the attribute value contained double quotes, use si singleQuote: false -If singleQuote is true, and the attribute value has liquid tags that contain single quotes, don't care. +It should keep Liquid single quotes inside single-quoted HTML attributes singleQuote: true -If singleQuote is false, and the attribute value has liquid tags that contain double quotes, don't care. +It should respect liquidSingleQuote inside double-quoted HTML attributes singleQuote: false @@ -107,3 +107,20 @@ It should pretty unquoted liquid drop attributes It should pretty print smart quotes into dumb quotes to prevent copy pasting errors

+ +It should respect liquidSingleQuote inside HTML attribute values +liquidSingleQuote: true +
+ +It should format Liquid string quotes inside an attribute without reformatting other Liquid markup +liquidSingleQuote: true +
+ +It should preserve Liquid strings that contain the preferred quote +liquidSingleQuote: true +
+ +It should respect liquidSingleQuote false inside HTML attribute values +liquidSingleQuote: false +singleQuote: true +