@shopify/prettier-plugin-liquid
Version:
Prettier Liquid/HTML plugin by Shopify
439 lines • 18.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.printerLiquidHtml3 = exports.printerLiquidHtml2 = void 0;
const liquid_html_parser_1 = require("@shopify/liquid-html-parser");
const prettier_1 = require("prettier");
const types_1 = require("../types");
const utils_1 = require("../utils");
const embed_1 = require("./embed");
const print_preprocess_1 = require("./print-preprocess");
const children_1 = require("./print/children");
const element_1 = require("./print/element");
const liquid_1 = require("./print/liquid");
const tag_1 = require("./print/tag");
const utils_2 = require("./utils");
const { builders, utils } = prettier_1.doc;
const { fill, group, hardline, dedentToRoot, indent, join, line, softline } = builders;
const oppositeQuotes = {
'"': "'",
"'": '"',
};
function printAttributeName(path, _options, print) {
const node = path.getValue();
node.name;
return join('', path.map((part) => {
const value = part.getValue();
if (typeof value === 'string') {
return value;
}
else {
// We want to force the LiquidVariableOutput to be on one line to avoid weird
// shenanigans
return utils.removeLines(print(part));
}
}, 'name'));
}
function printAttribute(path, options, print) {
const node = path.getValue();
const attrGroupId = Symbol('attr-group-id');
// What should be the rule here? Should it really be "paragraph"?
// ideally... if the thing is and the line is too long
// use cases:
// - attr-{{ section.id }}--something.
// * We should try to put that "block" on one line
//
// - attr {{ classname }} foo
// * we should try to put on one line?
//
// - attr hello world ok fellow friends what do
// * if the line becomes too long do we want to break one per line?
// - for alt, would be paragraph
// - for classes, yeah maybe
// - for srcset?, it should be "split on comma"
// - for sizes?, it should be "split on comma"
// - for href?, it should be no space url
// - for others?, it should be keywords
// - for style, should be break on ;
// - for other?, should be...
// - how the fuck am I going to do that?
// - same way we do this? with a big ass switch case?
// - or we... don't and leave it as is?
//
// Anyway, for that reason ^, for now I'll just paste in what we have in
// the source. It's too hard to get right.
const value = node.source.slice(node.attributePosition.start, node.attributePosition.end);
const preferredQuote = options.singleQuote ? `'` : `"`;
const attributeValueContainsQuote = !!node.value.find((valueNode) => (0, utils_2.isTextLikeNode)(valueNode) && valueNode.value.includes(preferredQuote));
const quote = attributeValueContainsQuote ? oppositeQuotes[preferredQuote] : preferredQuote;
return [
printAttributeName(path, options, print),
'=',
quote,
(0, utils_2.hasLineBreakInRange)(node.source, node.attributePosition.start, node.attributePosition.end)
? group([indent([softline, join(hardline, (0, utils_2.reindent)((0, utils_2.bodyLines)(value), true))]), softline], {
id: attrGroupId,
})
: value,
quote,
];
}
function isYamlFrontMatter(node) {
return (node.parentNode &&
node.parentNode.type === liquid_html_parser_1.NodeTypes.Document &&
!node.prev &&
/^---\r?\n/.test(node.value));
}
function printTextNode(path, options, _print) {
const node = path.getValue();
if (isYamlFrontMatter(node))
return node.value;
if (node.value.match(/^\s*$/))
return '';
const text = node.value;
const paragraphs = text
.split(/(\r?\n){2,}/)
.filter(Boolean) // removes empty paragraphs (trailingWhitespace)
.map((curr) => {
let doc = [];
const words = curr.trim().split(/\s+/g);
let isFirst = true;
for (let j = 0; j < words.length; j++) {
const word = words[j];
if (isFirst) {
isFirst = false;
}
else {
doc.push(line);
}
doc.push(word);
}
return fill(doc);
});
return [
(0, tag_1.printOpeningTagPrefix)(node, options),
join(hardline, paragraphs),
(0, tag_1.printClosingTagSuffix)(node, options),
];
}
function printNode(path, options, print, args = {}) {
var _a, _b;
const node = path.getValue();
switch (node.type) {
case liquid_html_parser_1.NodeTypes.Document: {
return [(0, children_1.printChildren)(path, options, print, args), hardline];
}
case liquid_html_parser_1.NodeTypes.HtmlElement: {
return (0, element_1.printElement)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.HtmlDanglingMarkerClose: {
return (0, element_1.printElement)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.HtmlVoidElement: {
return (0, element_1.printElement)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.HtmlSelfClosingElement: {
return (0, element_1.printElement)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.HtmlRawNode: {
return (0, element_1.printElement)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.RawMarkup: {
if (((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.name) === 'doc') {
return (0, liquid_1.printLiquidDoc)(path, options, print, args);
}
const isRawMarkupIdentationSensitive = () => {
switch (node.kind) {
case liquid_html_parser_1.RawMarkupKinds.typescript:
case liquid_html_parser_1.RawMarkupKinds.javascript: {
return node.value.includes('`');
}
default: {
return false;
}
}
};
if (isRawMarkupIdentationSensitive()) {
return [node.value.trimEnd(), hardline];
}
const parentNode = node.parentNode;
const shouldNotIndentBody = parentNode &&
parentNode.type === liquid_html_parser_1.NodeTypes.LiquidRawTag &&
parentNode.name === 'schema' &&
!options.indentSchema;
const shouldIndentBody = node.kind !== liquid_html_parser_1.RawMarkupKinds.markdown && !shouldNotIndentBody;
const lines = (0, utils_2.bodyLines)(node.value);
const rawFirstLineIsntIndented = !!((_b = node.value.split(/\r?\n/)[0]) === null || _b === void 0 ? void 0 : _b.match(/\S/));
const shouldSkipFirstLine = rawFirstLineIsntIndented;
const body = lines.length > 0 && lines.find((line) => line.trim() !== '')
? join(hardline, (0, utils_2.reindent)(lines, shouldSkipFirstLine))
: softline;
if (shouldIndentBody) {
return [indent([hardline, body]), hardline];
}
else {
return [dedentToRoot([hardline, body]), hardline];
}
}
case liquid_html_parser_1.NodeTypes.LiquidVariableOutput: {
return (0, liquid_1.printLiquidVariableOutput)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.LiquidRawTag: {
return (0, liquid_1.printLiquidRawTag)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.LiquidTag: {
return (0, liquid_1.printLiquidTag)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.LiquidBranch: {
return (0, liquid_1.printLiquidBranch)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.AttrEmpty: {
return printAttributeName(path, options, print);
}
case liquid_html_parser_1.NodeTypes.AttrUnquoted:
case liquid_html_parser_1.NodeTypes.AttrSingleQuoted:
case liquid_html_parser_1.NodeTypes.AttrDoubleQuoted: {
return printAttribute(path, options, print);
}
case liquid_html_parser_1.NodeTypes.HtmlDoctype: {
if (!node.legacyDoctypeString)
return '<!doctype html>';
return node.source.slice(node.position.start, node.position.end);
}
case liquid_html_parser_1.NodeTypes.HtmlComment: {
const conditionalComment = (0, liquid_html_parser_1.getConditionalComment)(node.source.slice(node.position.start, node.position.end));
if (conditionalComment) {
const { startTag, body, endTag } = conditionalComment;
return [
startTag,
group([indent([line, join(hardline, (0, utils_2.reindent)((0, utils_2.bodyLines)(body), true))]), line]),
endTag,
];
}
if (node.body.includes('prettier-ignore') ||
node.body.startsWith('display:') ||
node.body.startsWith('white-space:')) {
return node.source.slice(node.position.start, node.position.end);
}
return [
'<!--',
group([indent([line, join(hardline, (0, utils_2.reindent)((0, utils_2.bodyLines)(node.body), true))]), line]),
'-->',
];
}
case liquid_html_parser_1.NodeTypes.AssignMarkup: {
return [node.name, ' = ', path.call((p) => print(p), 'value')];
}
case liquid_html_parser_1.NodeTypes.CycleMarkup: {
const doc = [];
if (node.groupName) {
doc.push(path.call((p) => print(p), 'groupName'), ':');
}
const whitespace = node.args.length > 1 ? line : ' ';
doc.push(whitespace, join([',', whitespace], path.map((p) => print(p), 'args')));
return doc;
}
case liquid_html_parser_1.NodeTypes.ForMarkup: {
const doc = [node.variableName, ' in ', path.call((p) => print(p), 'collection')];
if (node.reversed) {
doc.push(line, 'reversed');
}
if (node.args.length > 0) {
doc.push([
line,
join(line, path.map((p) => print(p), 'args')),
]);
}
return doc;
}
case liquid_html_parser_1.NodeTypes.PaginateMarkup: {
const doc = [
path.call((p) => print(p), 'collection'),
line,
'by ',
path.call((p) => print(p), 'pageSize'),
];
if (node.args.length > 0) {
doc.push([
',',
line,
join([',', line], path.map((p) => print(p), 'args')),
]);
}
return doc;
}
case liquid_html_parser_1.NodeTypes.ContentForMarkup: {
const contentForType = path.call((p) => print(p), 'contentForType');
const doc = [contentForType];
if (node.args.length > 0) {
doc.push(',', line, join([',', line], path.map((p) => print(p), 'args')));
}
return doc;
}
case liquid_html_parser_1.NodeTypes.RenderMarkup: {
const snippet = path.call((p) => print(p), 'snippet');
const doc = [snippet];
if (node.variable) {
const whitespace = node.alias ? line : ' ';
doc.push(whitespace, path.call((p) => print(p), 'variable'));
}
if (node.alias) {
doc.push(' ', 'as', ' ', node.alias);
}
if (node.args.length > 0) {
doc.push(',', line, join([',', line], path.map((p) => print(p), 'args')));
}
return doc;
}
case liquid_html_parser_1.NodeTypes.RenderVariableExpression: {
return [node.kind, ' ', path.call((p) => print(p), 'name')];
}
case liquid_html_parser_1.NodeTypes.LogicalExpression: {
return [
path.call((p) => print(p), 'left'),
line,
node.relation,
' ',
path.call((p) => print(p), 'right'),
];
}
case liquid_html_parser_1.NodeTypes.Comparison: {
return group([
path.call((p) => print(p), 'left'),
indent([line, node.comparator, ' ', path.call((p) => print(p), 'right')]),
]);
}
case liquid_html_parser_1.NodeTypes.LiquidVariable: {
const name = path.call((p) => print(p), 'expression');
let filters = '';
if (node.filters.length > 0) {
filters = [
line,
join(line, path.map((p) => print(p), 'filters')),
];
}
return [name, filters];
}
case liquid_html_parser_1.NodeTypes.LiquidFilter: {
let args = [];
if (node.args.length > 0) {
const printed = path.map((p) => print(p), 'args');
const shouldPrintFirstArgumentSameLine = node.args[0].type !== liquid_html_parser_1.NodeTypes.NamedArgument;
if (shouldPrintFirstArgumentSameLine) {
const [firstDoc, ...rest] = printed;
const restDoc = (0, utils_2.isEmpty)(rest) ? '' : indent([',', line, join([',', line], rest)]);
args = [': ', firstDoc, restDoc];
}
else {
args = [':', indent([line, join([',', line], printed)])];
}
}
return group(['| ', node.name, ...args]);
}
case liquid_html_parser_1.NodeTypes.NamedArgument: {
return [node.name, ': ', path.call((p) => print(p), 'value')];
}
case liquid_html_parser_1.NodeTypes.TextNode: {
return printTextNode(path, options, print);
}
case liquid_html_parser_1.NodeTypes.YAMLFrontmatter: {
return ['---', hardline, node.body, '---'];
}
case liquid_html_parser_1.NodeTypes.String: {
const preferredQuote = options.liquidSingleQuote ? `'` : `"`;
const valueHasQuotes = node.value.includes(preferredQuote);
const quote = valueHasQuotes ? oppositeQuotes[preferredQuote] : preferredQuote;
return [quote, node.value, quote];
}
case liquid_html_parser_1.NodeTypes.Number: {
if (args.truncate) {
return node.value.replace(/\.\d+$/, '');
}
else {
return node.value;
}
}
case liquid_html_parser_1.NodeTypes.Range: {
return [
'(',
path.call((p) => print(p, { truncate: true }), 'start'),
'..',
path.call((p) => print(p, { truncate: true }), 'end'),
')',
];
}
case liquid_html_parser_1.NodeTypes.LiquidLiteral: {
// We prefer null over nil.
if (node.keyword === 'nil') {
return 'null';
}
return node.keyword;
}
case liquid_html_parser_1.NodeTypes.VariableLookup: {
const doc = [];
if (node.name) {
doc.push(node.name);
}
const lookups = path.map((lookupPath, index) => {
const lookup = lookupPath.getValue();
switch (lookup.type) {
case liquid_html_parser_1.NodeTypes.String: {
const value = lookup.value;
// We prefer direct access
// (for everything but stuff with dashes and stuff that starts with a number)
const isGlobalStringLookup = index === 0 && !node.name;
if (!isGlobalStringLookup && /^\D/.test(value) && /^[a-z0-9_]+\??$/i.test(value)) {
return ['.', value];
}
return ['[', print(lookupPath), ']'];
}
default: {
return ['[', print(lookupPath), ']'];
}
}
}, 'lookups');
return [...doc, ...lookups];
}
case liquid_html_parser_1.NodeTypes.LiquidDocParamNode: {
return (0, liquid_1.printLiquidDocParam)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.LiquidDocExampleNode: {
return (0, liquid_1.printLiquidDocExample)(path, options, print, args);
}
case liquid_html_parser_1.NodeTypes.LiquidDocDescriptionNode: {
return (0, liquid_1.printLiquidDocDescription)(path, options, print, args);
}
default: {
return (0, utils_1.assertNever)(node);
}
}
}
exports.printerLiquidHtml2 = {
print: printNode,
embed: embed_1.embed2,
preprocess: print_preprocess_1.preprocess,
getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter((key) => !nonTraversableKeys.has(key) &&
!types_1.nonTraversableProperties.has(key) &&
hasOrIsNode(node, key));
},
};
exports.printerLiquidHtml3 = {
print: printNode,
embed: embed_1.embed3,
preprocess: print_preprocess_1.preprocess,
getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter((key) => !nonTraversableKeys.has(key) &&
!types_1.nonTraversableProperties.has(key) &&
hasOrIsNode(node, key));
},
};
function hasOrIsNode(node, key) {
const v = node[key];
// this works because there's no ()[] type that is string | Node, it only
// happens for singular nodes such as name: string | LiquidDrop, etc.
return Array.isArray(v) || isNode(v);
}
function isNode(x) {
return x !== null && typeof x === 'object' && 'type' in x && typeof x.type === 'string';
}
//# sourceMappingURL=printer-liquid-html.js.map