@shopify/prettier-plugin-liquid
Version:
Prettier Liquid/HTML plugin by Shopify
546 lines • 22.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.printLiquidBranch = exports.printLiquidDocDescription = exports.printLiquidDocExample = exports.printLiquidDocParam = exports.printLiquidDoc = exports.printLiquidRawTag = exports.printLiquidTag = exports.printLiquidBlockEnd = exports.printLiquidBlockStart = exports.printLiquidVariableOutput = void 0;
const liquid_html_parser_1 = require("@shopify/liquid-html-parser");
const prettier_1 = require("prettier");
const utils_1 = require("../../utils");
const utils_2 = require("../utils");
const children_1 = require("./children");
const LIQUID_TAGS_THAT_ALWAYS_BREAK = ['for', 'case'];
const { builders, utils } = prettier_1.doc;
const { group, hardline, ifBreak, indent, join, line, softline, literalline } = builders;
const { replaceEndOfLine } = prettier_1.doc.utils;
function printLiquidVariableOutput(path, _options, print, { leadingSpaceGroupId, trailingSpaceGroupId }) {
const node = path.getValue();
const whitespaceStart = (0, utils_2.getWhitespaceTrim)(node.whitespaceStart, (0, utils_2.hasMeaningfulLackOfLeadingWhitespace)(node), leadingSpaceGroupId);
const whitespaceEnd = (0, utils_2.getWhitespaceTrim)(node.whitespaceEnd, (0, utils_2.hasMeaningfulLackOfTrailingWhitespace)(node), trailingSpaceGroupId);
if (typeof node.markup !== 'string') {
const whitespace = node.markup.filters.length > 0 ? line : ' ';
return group([
'{{',
whitespaceStart,
indent([whitespace, path.call((p) => print(p), 'markup')]),
whitespace,
whitespaceEnd,
'}}',
]);
}
// This should probably be better than this but it'll do for now.
const lines = (0, utils_2.markupLines)(node.markup);
if (lines.length > 1) {
return group([
'{{',
whitespaceStart,
indent([hardline, join(hardline, lines.map(utils_2.trim))]),
hardline,
whitespaceEnd,
'}}',
]);
}
return group(['{{', whitespaceStart, ' ', node.markup, ' ', whitespaceEnd, '}}']);
}
exports.printLiquidVariableOutput = printLiquidVariableOutput;
function printNamedLiquidBlockStart(path, _options, print, args, whitespaceStart, whitespaceEnd) {
const node = path.getValue();
const { isLiquidStatement } = args;
// This is slightly more verbose than 3 ternaries, but I feel like I
// should make it obvious that these three things work in tandem on the
// same conditional.
const { wrapper, prefix, suffix } = (() => {
if (isLiquidStatement) {
return {
wrapper: utils.removeLines,
prefix: '',
suffix: () => '',
};
}
else {
return {
wrapper: group,
prefix: ['{%', whitespaceStart, ' '],
suffix: (trailingWhitespace) => [trailingWhitespace, whitespaceEnd, '%}'],
};
}
})();
const tag = (trailingWhitespace) => wrapper([
...prefix,
node.name,
' ',
indent(path.call((p) => print(p, args), 'markup')),
...suffix(trailingWhitespace),
]);
const tagWithArrayMarkup = (whitespace) => wrapper([
...prefix,
node.name,
' ',
indent([
join([',', line], path.map((p) => print(p, args), 'markup')),
]),
...suffix(whitespace),
]);
switch (node.name) {
case liquid_html_parser_1.NamedTags.echo: {
const trailingWhitespace = node.markup.filters.length > 0 ? line : ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.assign: {
const trailingWhitespace = node.markup.value.filters.length > 0 ? line : ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.cycle: {
const whitespace = node.markup.args.length > 1 ? line : ' ';
return wrapper([
...prefix,
node.name,
// We want to break after the groupName
node.markup.groupName ? ' ' : '',
indent(path.call((p) => print(p, args), 'markup')),
...suffix(whitespace),
]);
}
case liquid_html_parser_1.NamedTags.content_for: {
const markup = node.markup;
const trailingWhitespace = markup.args.length > 0 ? line : ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.include:
case liquid_html_parser_1.NamedTags.render: {
const markup = node.markup;
const trailingWhitespace = markup.args.length > 0 || (markup.variable && markup.alias) ? line : ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.capture:
case liquid_html_parser_1.NamedTags.increment:
case liquid_html_parser_1.NamedTags.decrement:
case liquid_html_parser_1.NamedTags.layout:
case liquid_html_parser_1.NamedTags.section: {
return tag(' ');
}
case liquid_html_parser_1.NamedTags.sections: {
return tag(' ');
}
case liquid_html_parser_1.NamedTags.form: {
const trailingWhitespace = node.markup.length > 1 ? line : ' ';
return tagWithArrayMarkup(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.tablerow:
case liquid_html_parser_1.NamedTags.for: {
const trailingWhitespace = node.markup.reversed || node.markup.args.length > 0 ? line : ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.paginate: {
return tag(line);
}
case liquid_html_parser_1.NamedTags.if:
case liquid_html_parser_1.NamedTags.elsif:
case liquid_html_parser_1.NamedTags.unless: {
const trailingWhitespace = [liquid_html_parser_1.NodeTypes.Comparison, liquid_html_parser_1.NodeTypes.LogicalExpression].includes(node.markup.type)
? line
: ' ';
return tag(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.case: {
return tag(' ');
}
case liquid_html_parser_1.NamedTags.when: {
const trailingWhitespace = node.markup.length > 1 ? line : ' ';
return tagWithArrayMarkup(trailingWhitespace);
}
case liquid_html_parser_1.NamedTags.liquid: {
return group([
...prefix,
node.name,
indent([
hardline,
join(hardline, path.map((p) => {
const curr = p.getValue();
return [
getSpaceBetweenLines(curr.prev, curr),
print(p, Object.assign(Object.assign({}, args), { isLiquidStatement: true })),
];
}, 'markup')),
]),
...suffix(hardline),
]);
}
default: {
return (0, utils_1.assertNever)(node);
}
}
}
function printLiquidStatement(path, _options, _print, _args) {
const node = path.getValue();
const shouldSkipLeadingSpace = node.markup.trim() === '' || (node.name === '#' && node.markup.startsWith('#'));
return prettier_1.doc.utils.removeLines([node.name, shouldSkipLeadingSpace ? '' : ' ', node.markup]);
}
function printLiquidBlockStart(path, options, print, args = {}) {
const node = path.getValue();
const { leadingSpaceGroupId, trailingSpaceGroupId } = args;
if (!node.name)
return '';
const whitespaceStart = (0, utils_2.getWhitespaceTrim)(node.whitespaceStart, needsBlockStartLeadingWhitespaceStrippingOnBreak(node), leadingSpaceGroupId);
const whitespaceEnd = (0, utils_2.getWhitespaceTrim)(node.whitespaceEnd, needsBlockStartTrailingWhitespaceStrippingOnBreak(node), trailingSpaceGroupId);
if (typeof node.markup !== 'string') {
return printNamedLiquidBlockStart(path, options, print, args, whitespaceStart, whitespaceEnd);
}
if (args.isLiquidStatement) {
return printLiquidStatement(path, options, print, args);
}
const lines = (0, utils_2.markupLines)(node.markup);
if (node.name === 'liquid') {
return group([
'{%',
whitespaceStart,
' ',
node.name,
indent([hardline, join(hardline, (0, utils_2.reindent)(lines, true))]),
hardline,
whitespaceEnd,
'%}',
]);
}
if (lines.length > 1) {
return group([
'{%',
whitespaceStart,
indent([hardline, node.name, ' ', join(hardline, lines.map(utils_2.trim))]),
hardline,
whitespaceEnd,
'%}',
]);
}
const markup = node.markup;
return group([
'{%',
whitespaceStart,
' ',
node.name,
markup ? ` ${markup}` : '',
' ',
whitespaceEnd,
'%}',
]);
}
exports.printLiquidBlockStart = printLiquidBlockStart;
function printLiquidBlockEnd(path, _options, _print, args = {}) {
var _a, _b;
const node = path.getValue();
const { isLiquidStatement, leadingSpaceGroupId, trailingSpaceGroupId } = args;
if (!node.children || !node.blockEndPosition)
return '';
if (isLiquidStatement) {
return ['end', node.name];
}
const whitespaceStart = (0, utils_2.getWhitespaceTrim)((_a = node.delimiterWhitespaceStart) !== null && _a !== void 0 ? _a : '', needsBlockEndLeadingWhitespaceStrippingOnBreak(node), leadingSpaceGroupId);
const whitespaceEnd = (0, utils_2.getWhitespaceTrim)((_b = node.delimiterWhitespaceEnd) !== null && _b !== void 0 ? _b : '', (0, utils_2.hasMeaningfulLackOfTrailingWhitespace)(node), trailingSpaceGroupId);
return group(['{%', whitespaceStart, ` end${node.name} `, whitespaceEnd, '%}']);
}
exports.printLiquidBlockEnd = printLiquidBlockEnd;
function getNodeContent(node) {
if (!node.children || !node.blockEndPosition)
return '';
return node.source.slice(node.blockStartPosition.end, node.blockEndPosition.start);
}
function printLiquidTag(path, options, print, args) {
const { leadingSpaceGroupId, trailingSpaceGroupId } = args;
const node = path.getValue();
if (!node.children || !node.blockEndPosition) {
return printLiquidBlockStart(path, options, print, args);
}
if (!args.isLiquidStatement && (0, utils_2.shouldPreserveContent)(node)) {
return [
printLiquidBlockStart(path, options, print, Object.assign(Object.assign({}, args), { leadingSpaceGroupId, trailingSpaceGroupId: utils_2.FORCE_FLAT_GROUP_ID })),
...replaceEndOfLine(getNodeContent(node)),
printLiquidBlockEnd(path, options, print, Object.assign(Object.assign({}, args), { leadingSpaceGroupId: utils_2.FORCE_FLAT_GROUP_ID, trailingSpaceGroupId })),
];
}
const tagGroupId = Symbol('tag-group');
const blockStart = printLiquidBlockStart(path, options, print, Object.assign(Object.assign({}, args), { leadingSpaceGroupId, trailingSpaceGroupId: tagGroupId })); // {% if ... %}
const blockEnd = printLiquidBlockEnd(path, options, print, Object.assign(Object.assign({}, args), { leadingSpaceGroupId: tagGroupId, trailingSpaceGroupId })); // {% endif %}
let body = [];
if ((0, liquid_html_parser_1.isBranchedTag)(node)) {
body = cleanDoc(path.map((p) => print(p, Object.assign(Object.assign({}, args), { leadingSpaceGroupId: tagGroupId, trailingSpaceGroupId: tagGroupId })), 'children'));
if (node.name === 'case')
body = indent(body);
}
else if (node.children.length > 0) {
body = indent([
innerLeadingWhitespace(node),
(0, children_1.printChildren)(path, options, print, Object.assign(Object.assign({}, args), { leadingSpaceGroupId: tagGroupId, trailingSpaceGroupId: tagGroupId })),
]);
}
return group([blockStart, body, innerTrailingWhitespace(node, args), blockEnd], {
id: tagGroupId,
shouldBreak: LIQUID_TAGS_THAT_ALWAYS_BREAK.includes(node.name) ||
(0, utils_2.originallyHadLineBreaks)(path, options) ||
(0, utils_2.isAttributeNode)(node) ||
(0, utils_2.isDeeplyNested)(node),
});
}
exports.printLiquidTag = printLiquidTag;
function printLiquidRawTag(path, options, print, { isLiquidStatement }) {
let body = [];
const node = path.getValue();
const hasEmptyBody = node.body.value.trim() === '';
const shouldPrintAsIs = node.isIndentationSensitive ||
!(0, utils_2.hasLineBreakInRange)(node.source, node.body.position.start, node.body.position.end);
const blockStart = isLiquidStatement
? [node.name]
: group([
'{%',
node.whitespaceStart,
' ',
node.name,
' ',
node.markup ? `${node.markup} ` : '',
node.whitespaceEnd,
'%}',
]);
const blockEnd = isLiquidStatement
? ['end', node.name]
: ['{%', node.whitespaceStart, ' ', 'end', node.name, ' ', node.whitespaceEnd, '%}'];
if (shouldPrintAsIs) {
body = [node.source.slice(node.blockStartPosition.end, node.blockEndPosition.start)];
}
else if (hasEmptyBody) {
body = [hardline];
}
else {
body = [path.call((p) => print(p), 'body')];
}
return [blockStart, ...body, blockEnd];
}
exports.printLiquidRawTag = printLiquidRawTag;
function printLiquidDoc(path, _options, print, _args) {
const nodes = path.map((p) => print(p), 'nodes');
if (nodes.length === 0)
return [];
const lines = [nodes[0]];
for (let i = 1; i < nodes.length; i++) {
lines.push(hardline);
// If the tag name is different from the previous one, add an extra line break
if (nodes[i - 1][0] !== nodes[i][0]) {
lines.push(hardline);
}
lines.push(nodes[i]);
}
return [indent([hardline, lines]), hardline];
}
exports.printLiquidDoc = printLiquidDoc;
function printLiquidDocParam(path, options, _print, _args) {
var _a, _b;
const node = path.getValue();
const parts = ['@param'];
if ((_a = node.paramType) === null || _a === void 0 ? void 0 : _a.value) {
parts.push(' ', `{${node.paramType.value}}`);
}
if (node.required) {
parts.push(' ', node.paramName.value);
}
else {
parts.push(' ', `[${node.paramName.value}]`);
}
if ((_b = node.paramDescription) === null || _b === void 0 ? void 0 : _b.value) {
const normalizedDescription = node.paramDescription.value.replace(/\s+/g, ' ').trim();
if (options.liquidDocParamDash) {
parts.push(' - ', normalizedDescription);
}
else {
parts.push(' ', normalizedDescription);
}
}
return parts;
}
exports.printLiquidDocParam = printLiquidDocParam;
function printLiquidDocExample(path, options, _print, _args) {
const node = path.getValue();
const parts = ['@example'];
const content = node.content.value;
if (content.trimEnd().includes('\n') || !node.isInline) {
parts.push(hardline);
}
else {
parts.push(' ');
}
parts.push(content.trim());
return parts;
}
exports.printLiquidDocExample = printLiquidDocExample;
function printLiquidDocDescription(path, options, _print, _args) {
const node = path.getValue();
const parts = [];
const content = node.content.value;
if (node.isImplicit) {
parts.push(content.trim());
return parts;
}
parts.push('@description');
if (content.trimEnd().includes('\n') || !node.isInline) {
parts.push(hardline);
}
else {
parts.push(' ');
}
parts.push(content.trim());
return parts;
}
exports.printLiquidDocDescription = printLiquidDocDescription;
function innerLeadingWhitespace(node) {
if (!node.firstChild) {
if (node.isDanglingWhitespaceSensitive && node.hasDanglingWhitespace) {
return line;
}
else {
return '';
}
}
if (node.firstChild.hasLeadingWhitespace && node.firstChild.isLeadingWhitespaceSensitive) {
return line;
}
return softline;
}
function innerTrailingWhitespace(node, args) {
if ((!args.isLiquidStatement && (0, utils_2.shouldPreserveContent)(node)) ||
node.type === liquid_html_parser_1.NodeTypes.LiquidBranch ||
!node.blockEndPosition ||
!node.lastChild) {
return '';
}
if (node.lastChild.hasTrailingWhitespace && node.lastChild.isTrailingWhitespaceSensitive) {
return line;
}
return softline;
}
function printLiquidDefaultBranch(path, options, print, args) {
const branch = path.getValue();
const parentNode = path.getParentNode();
// When the node is empty and the parent is empty. The space will come
// from the trailingWhitespace of the parent. When this happens, we don't
// want the branch to print another one so we collapse it.
// e.g. {% if A %} {% endif %}
const shouldCollapseSpace = (0, utils_2.isEmpty)(branch.children) && parentNode.children.length === 1;
if (shouldCollapseSpace)
return '';
// When the branch is empty and doesn't have whitespace, we don't want
// anything so print nothing.
// e.g. {% if A %}{% endif %}
// e.g. {% if A %}{% else %}...{% endif %}
const isBranchEmptyWithoutSpace = (0, utils_2.isEmpty)(branch.children) && !branch.hasDanglingWhitespace;
if (isBranchEmptyWithoutSpace)
return '';
// If the branch does not break, is empty and had whitespace, we might
// want a space in there. We don't collapse those because the trailing
// whitespace does not come from the parent.
// {% if A %} {% else %}...{% endif %}
if (branch.hasDanglingWhitespace) {
return ifBreak('', ' ');
}
const shouldAddTrailingNewline = branch.next &&
branch.children.length > 0 &&
branch.source
.slice((0, utils_2.last)(branch.children).position.end, branch.next.position.start)
.replace(/ |\t/g, '').length >= 2;
// Otherwise print the branch as usual
// {% if A %} content...{% endif %}
return indent([
innerLeadingWhitespace(parentNode),
(0, children_1.printChildren)(path, options, print, args),
shouldAddTrailingNewline ? literalline : '',
]);
}
function printLiquidBranch(path, options, print, args) {
const branch = path.getValue();
const isDefaultBranch = !branch.name;
if (isDefaultBranch) {
return printLiquidDefaultBranch(path, options, print, args);
}
const leftSibling = branch.prev;
// When the left sibling is empty, its trailing whitespace is its leading
// whitespace. So we should collapse it here and ignore it.
const shouldCollapseSpace = leftSibling && (0, utils_2.isEmpty)(leftSibling.children);
const outerLeadingWhitespace = branch.hasLeadingWhitespace && !shouldCollapseSpace ? line : softline;
const shouldAddTrailingNewline = branch.next &&
branch.children.length > 0 &&
branch.source
.slice((0, utils_2.last)(branch.children).position.end, branch.next.position.start)
.replace(/ |\t/g, '').length >= 2;
return [
outerLeadingWhitespace,
printLiquidBlockStart(path, options, print, args),
indent([
innerLeadingWhitespace(branch),
(0, children_1.printChildren)(path, options, print, args),
shouldAddTrailingNewline ? literalline : '',
]),
];
}
exports.printLiquidBranch = printLiquidBranch;
function needsBlockStartLeadingWhitespaceStrippingOnBreak(node) {
switch (node.type) {
case liquid_html_parser_1.NodeTypes.LiquidTag: {
return !(0, utils_2.isAttributeNode)(node) && (0, utils_2.hasMeaningfulLackOfLeadingWhitespace)(node);
}
case liquid_html_parser_1.NodeTypes.LiquidBranch: {
return (!(0, utils_2.isAttributeNode)(node.parentNode) &&
(0, utils_2.hasMeaningfulLackOfLeadingWhitespace)(node));
}
default: {
return (0, utils_1.assertNever)(node);
}
}
}
function needsBlockStartTrailingWhitespaceStrippingOnBreak(node) {
switch (node.type) {
case liquid_html_parser_1.NodeTypes.LiquidTag: {
if ((0, liquid_html_parser_1.isBranchedTag)(node)) {
return needsBlockStartLeadingWhitespaceStrippingOnBreak(node.firstChild);
}
if (!node.children) {
return (0, utils_2.hasMeaningfulLackOfTrailingWhitespace)(node);
}
return (0, utils_2.isEmpty)(node.children)
? (0, utils_2.hasMeaningfulLackOfDanglingWhitespace)(node)
: (0, utils_2.hasMeaningfulLackOfLeadingWhitespace)(node.firstChild);
}
case liquid_html_parser_1.NodeTypes.LiquidBranch: {
if ((0, utils_2.isAttributeNode)(node.parentNode)) {
return false;
}
return node.firstChild
? (0, utils_2.hasMeaningfulLackOfLeadingWhitespace)(node.firstChild)
: (0, utils_2.hasMeaningfulLackOfDanglingWhitespace)(node);
}
default: {
return (0, utils_1.assertNever)(node);
}
}
}
function needsBlockEndLeadingWhitespaceStrippingOnBreak(node) {
if (!node.children) {
throw new Error('Should only call needsBlockEndLeadingWhitespaceStrippingOnBreak for tags that have closing tags');
}
else if ((0, utils_2.isAttributeNode)(node)) {
return false;
}
else if ((0, liquid_html_parser_1.isBranchedTag)(node)) {
return (0, utils_2.hasMeaningfulLackOfTrailingWhitespace)(node.lastChild);
}
else if ((0, utils_2.isEmpty)(node.children)) {
return (0, utils_2.hasMeaningfulLackOfDanglingWhitespace)(node);
}
else {
return (0, utils_2.hasMeaningfulLackOfTrailingWhitespace)(node.lastChild);
}
}
function cleanDoc(doc) {
return doc.filter((x) => x !== '');
}
function getSpaceBetweenLines(prev, curr) {
if (!prev)
return '';
const source = curr.source;
const whitespaceBetweenNodes = source.slice(prev.position.end, curr.position.start);
const hasMoreThanOneNewLine = (whitespaceBetweenNodes.match(/\n/g) || []).length > 1;
return hasMoreThanOneNewLine ? hardline : '';
}
//# sourceMappingURL=liquid.js.map