Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playground/assets/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const options = {
};

const numInputs = ['printWidth'];
const boolInputs = ['liquidSingleQuote', 'singleQuote'];
const boolInputs = ['twigSingleQuote', 'singleQuote'];
const selectInputs = ['htmlWhitespaceSensitivity'];

const waitFor = (pred) => {
Expand Down
6 changes: 3 additions & 3 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ <h1>
<label title="Use single quotes instead of double quotes.">
<input id="singleQuote" type="checkbox" /> --single-quote
</label>
<label title="Use single quotes in Liquid.">
<input id="liquidSingleQuote" type="checkbox" checked />
--liquid-single-quote
<label title="Use single quotes in Twig.">
<input id="twigSingleQuote" type="checkbox" checked />
--twig-single-quote
</label>
</details>
</div>
Expand Down
12 changes: 10 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ const languages: SupportLanguage[] = [
];

const options: SupportOptions = {
liquidSingleQuote: {
twigSingleQuote: {
type: 'boolean',
category: 'LIQUID',
default: true,
description:
'Use single quotes instead of double quotes in Liquid tags and objects.',
'Use single quotes instead of double quotes in Twig tags and objects.',
since: '0.2.0',
},
liquidSingleQuote: {
type: 'boolean',
category: 'LIQUID',
default: undefined,
description:
'Deprecated: Use twigSingleQuote instead. Use single quotes instead of double quotes in Twig tags and objects.',
since: '0.2.0',
},
embeddedSingleQuote: {
Expand Down
15 changes: 9 additions & 6 deletions src/printer/print/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
LiquidStatement,
} from '~/types';
import { isBranchedTag } from '~/parser/stage-2-ast';
import { assertNever } from '~/utils';
import { assertNever, getTwigSingleQuote } from '~/utils';

import {
getWhitespaceTrim,
Expand Down Expand Up @@ -76,8 +76,11 @@ export function printLiquidDrop(
]);
}

// Transform quotes in base case markup based on liquidSingleQuote option
const markup = transformStringQuotes(node.markup, options.liquidSingleQuote);
// Transform quotes in base case markup based on twigSingleQuote option
const markup = transformStringQuotes(
node.markup,
getTwigSingleQuote(options),
);

// This should probably be better than this but it'll do for now.
const lines = markupLines(markup);
Expand Down Expand Up @@ -271,7 +274,7 @@ function printLiquidStatement(
const node = path.getValue();
const transformedMarkup = transformStringQuotes(
node.markup,
options.liquidSingleQuote,
getTwigSingleQuote(options),
);
const shouldSkipLeadingSpace =
transformedMarkup.trim() === '' ||
Expand Down Expand Up @@ -349,10 +352,10 @@ export function printLiquidBlockStart(
]);
}

// Transform quotes in base case markup based on liquidSingleQuote option
// Transform quotes in base case markup based on twigSingleQuote option
const transformedMarkup = transformStringQuotes(
node.markup,
options.liquidSingleQuote,
getTwigSingleQuote(options),
);
const lines = markupLines(transformedMarkup);

Expand Down
4 changes: 2 additions & 2 deletions src/printer/printer-liquid-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
nonTraversableProperties,
AstPath,
} from '~/types';
import { assertNever } from '~/utils';
import { assertNever, getTwigSingleQuote } from '~/utils';

import { preprocess } from '~/printer/print-preprocess';
import {
Expand Down Expand Up @@ -524,7 +524,7 @@ function printNode(
}

case NodeTypes.String: {
const preferredQuote = options.liquidSingleQuote ? `'` : `"`;
const preferredQuote = getTwigSingleQuote(options) ? `'` : `"`;
const valueHasQuotes = node.value.includes(preferredQuote);
const quote = valueHasQuotes
? oppositeQuotes[preferredQuote]
Expand Down
8 changes: 4 additions & 4 deletions src/printer/utils/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function hasMoreThanOneNewLineBetweenNodes(
}

/**
* Transforms quotes in base case markup strings based on the liquidSingleQuote option.
* Transforms quotes in base case markup strings based on the twigSingleQuote 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')`).
*
Expand All @@ -78,13 +78,13 @@ export function hasMoreThanOneNewLineBetweenNodes(
*/
export function transformStringQuotes(
markup: string,
liquidSingleQuote: boolean,
twigSingleQuote: boolean,
): string {
const preferredQuote = liquidSingleQuote ? "'" : '"';
const preferredQuote = twigSingleQuote ? "'" : '"';

// Match strings with the non-preferred quote style
// This regex matches quoted strings, being careful about escapes
const stringRegex = liquidSingleQuote
const stringRegex = twigSingleQuote
? /"([^"\\]|\\.)*"/g // Match double-quoted strings
: /'([^'\\]|\\.)*'/g; // Match single-quoted strings

Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ export type LiquidAstPath = AstPath<LiquidHtmlNode>;
export type LiquidParserOptions = ParserOptions<LiquidHtmlNode> & {
singleAttributePerLine: boolean;
singleLineLinkTags: boolean;
liquidSingleQuote: boolean;
twigSingleQuote: boolean;
liquidSingleQuote?: boolean;
embeddedSingleQuote: boolean;
indentSchema: boolean;
};
Expand Down
31 changes: 31 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,34 @@ export function dropLast<T>(n: number, xs: readonly T[]) {
}
return result;
}

let hasWarnedLiquidSingleQuote = false;

/**
* Resets the deprecation warning state. Only used for testing.
* @internal
*/
export function resetTwigSingleQuoteWarning(): void {
hasWarnedLiquidSingleQuote = false;
}

/**
* Gets the effective twigSingleQuote value, supporting the deprecated liquidSingleQuote option.
* Shows a deprecation warning once if liquidSingleQuote is used.
*/
export function getTwigSingleQuote(options: {
twigSingleQuote: boolean;
liquidSingleQuote?: boolean;
}): boolean {
// If liquidSingleQuote is explicitly set (not undefined), use it with deprecation warning
if (options.liquidSingleQuote !== undefined) {
if (!hasWarnedLiquidSingleQuote) {
hasWarnedLiquidSingleQuote = true;
console.warn(
'[prettier-plugin-twig] The "liquidSingleQuote" option is deprecated. Please use "twigSingleQuote" instead.',
);
}
return options.liquidSingleQuote;
}
return options.twigSingleQuote;
}
2 changes: 1 addition & 1 deletion test/liquid-drop-string/fixed.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ It should keep double quotes if the string includes single quotes
{{ "string o' string" }}

It should maintain whitespace stripping characters and respect the singleQuote config
liquidSingleQuote: false
twigSingleQuote: false
{{- "hello" -}}
2 changes: 1 addition & 1 deletion test/liquid-drop-string/index.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ It should keep double quotes if the string includes single quotes
{{ "string o' string" }}

It should maintain whitespace stripping characters and respect the singleQuote config
liquidSingleQuote: false
twigSingleQuote: false
{{- 'hello' -}}
2 changes: 1 addition & 1 deletion test/liquid-drop-variable-lookup/fixed.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ It should index access strings with spaces
{{ x.y['hello world'] }}

It should respect singleQuote
liquidSingleQuote: false
twigSingleQuote: false
{{ x.y["hello world"] }}

It should keep double quotes if single quotes are in the string
Expand Down
2 changes: 1 addition & 1 deletion test/liquid-drop-variable-lookup/index.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ It should index access strings with spaces
{{ x["y"]["hello world"] }}

It should respect singleQuote
liquidSingleQuote: false
twigSingleQuote: false
{{ x["y"]["hello world"] }}

It should keep double quotes if single quotes are in the string
Expand Down
4 changes: 2 additions & 2 deletions test/liquid-tag-form/fixed.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ printWidth: 1
hello
{% endform %}

It can take named arguments, and respects liquidSingleQuote
It can take named arguments, and respects twigSingleQuote
printWidth: 1
{% form 'cart',
key: value
%}{% endform %}

It can take named arguments, and respects liquidSingleQuote (pw80)
It can take named arguments, and respects twigSingleQuote (pw80)
printWidth: 80
{% form 'cart', key1: value1, key2: 'value2' %}{% endform %}

Expand Down
4 changes: 2 additions & 2 deletions test/liquid-tag-form/index.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ printWidth: 1
{% form x %}{% endform %}
{% form x %}hello{% endform %}

It can take named arguments, and respects liquidSingleQuote
It can take named arguments, and respects twigSingleQuote
printWidth: 1
{% form "cart",key:value%}{% endform %}

It can take named arguments, and respects liquidSingleQuote (pw80)
It can take named arguments, and respects twigSingleQuote (pw80)
printWidth: 80
{% form "cart",key1:value1, key2:'value2' %} {% endform %}

Expand Down
8 changes: 4 additions & 4 deletions test/twig-function-quotes/fixed.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ It should convert double quotes to single quotes in tag base case
It should handle multiple arguments with quotes
{{ some_function('arg1', 'arg2') }}

It should convert to double quotes when liquidSingleQuote is false
liquidSingleQuote: false
It should convert to double quotes when twigSingleQuote is false
twigSingleQuote: false
{{ stimulus_controller("controller-name") }}

It should keep single quotes if string contains double quotes when liquidSingleQuote is false
liquidSingleQuote: false
It should keep single quotes if string contains double quotes when twigSingleQuote is false
twigSingleQuote: false
{{ stimulus_controller('say "hello"') }}

It should not modify HTML attributes within Twig comments
Expand Down
8 changes: 4 additions & 4 deletions test/twig-function-quotes/index.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ It should convert double quotes to single quotes in tag base case
It should handle multiple arguments with quotes
{{ some_function("arg1", "arg2") }}

It should convert to double quotes when liquidSingleQuote is false
liquidSingleQuote: false
It should convert to double quotes when twigSingleQuote is false
twigSingleQuote: false
{{ stimulus_controller('controller-name') }}

It should keep single quotes if string contains double quotes when liquidSingleQuote is false
liquidSingleQuote: false
It should keep single quotes if string contains double quotes when twigSingleQuote is false
twigSingleQuote: false
{{ stimulus_controller('say "hello"') }}

It should not modify HTML attributes within Twig comments
Expand Down
81 changes: 81 additions & 0 deletions test/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect } from 'chai';
import { getTwigSingleQuote, resetTwigSingleQuoteWarning } from 'src/utils';

describe('Module: utils', () => {
describe('Unit: getTwigSingleQuote', () => {
let warnCalls: string[];
let originalWarn: typeof console.warn;

beforeEach(() => {
warnCalls = [];
originalWarn = console.warn;
console.warn = (message: string) => {
warnCalls.push(message);
};
// Reset the warning state before each test
resetTwigSingleQuoteWarning();
});

afterEach(() => {
console.warn = originalWarn;
});

it('should return twigSingleQuote when liquidSingleQuote is not set', () => {
expect(getTwigSingleQuote({ twigSingleQuote: true })).to.equal(true);
expect(getTwigSingleQuote({ twigSingleQuote: false })).to.equal(false);
expect(warnCalls.length).to.equal(0);
});

it('should return twigSingleQuote when liquidSingleQuote is undefined', () => {
expect(getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: undefined })).to.equal(
true,
);
expect(getTwigSingleQuote({ twigSingleQuote: false, liquidSingleQuote: undefined })).to.equal(
false,
);
expect(warnCalls.length).to.equal(0);
});

it('should return liquidSingleQuote when it is explicitly set', () => {
expect(getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: false })).to.equal(
false,
);

resetTwigSingleQuoteWarning();

expect(getTwigSingleQuote({ twigSingleQuote: false, liquidSingleQuote: true })).to.equal(
true,
);
});

it('should show deprecation warning when liquidSingleQuote is explicitly set', () => {
getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: false });

expect(warnCalls.length).to.equal(1);
expect(warnCalls[0]).to.include('liquidSingleQuote');
expect(warnCalls[0]).to.include('deprecated');
expect(warnCalls[0]).to.include('twigSingleQuote');
});

it('should only show deprecation warning once per session', () => {
getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: false });
getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: false });
getTwigSingleQuote({ twigSingleQuote: false, liquidSingleQuote: true });

expect(warnCalls.length).to.equal(1);
});

it('should prefer liquidSingleQuote over twigSingleQuote for backwards compatibility', () => {
// When both are set, liquidSingleQuote should win
expect(getTwigSingleQuote({ twigSingleQuote: true, liquidSingleQuote: false })).to.equal(
false,
);

resetTwigSingleQuoteWarning();

expect(getTwigSingleQuote({ twigSingleQuote: false, liquidSingleQuote: true })).to.equal(
true,
);
});
});
});