eslint-plugin-better-tailwindcss
Version:
auto-wraps tailwind classes after a certain print width or class count into multiple lines to improve readability.
423 lines • 16.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAttributesByAngularElement = getAttributesByAngularElement;
exports.getLiteralsByAngularAttribute = getLiteralsByAngularAttribute;
exports.isInsideConditionalExpressionCondition = isInsideConditionalExpressionCondition;
exports.isInsideLogicalExpressionLeft = isInsideLogicalExpressionLeft;
exports.findParent = findParent;
exports.isAST = isAST;
const rule_js_1 = require("../types/rule.js");
const matchers_js_1 = require("../utils/matchers.js");
const utils_js_1 = require("../utils/utils.js");
// https://angular.dev/api/common/NgClass
// https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
// TODO:
// - Implement regex
// - Add object keys
function getAttributesByAngularElement(ctx, node) {
return [
...node.attributes,
...node.inputs
];
}
function getLiteralsByAngularAttribute(ctx, attribute, attributes) {
const literals = attributes.reduce((literals, attributes) => {
if ((0, matchers_js_1.isAttributesName)(attributes)) {
if (!(0, utils_js_1.matchesName)(attributes.toLowerCase(), getAttributeName(attribute).toLowerCase())) {
return literals;
}
literals.push(...createLiteralsByAngularAttribute(ctx, attribute));
}
else if ((0, matchers_js_1.isAttributesRegex)(attributes)) {
// console.warn("Regex not supported for now");
}
else if ((0, matchers_js_1.isAttributesMatchers)(attributes)) {
if (!(0, utils_js_1.matchesName)(attributes[0].toLowerCase(), getAttributeName(attribute).toLowerCase())) {
return literals;
}
if (isTextAttribute(attribute)) {
literals.push(...createLiteralsByAngularTextAttribute(ctx, attribute));
}
if (isBoundAttribute(attribute) && isASTWithSource(attribute.value)) {
literals.push(...getLiteralsByAngularMatchers(ctx, attribute.value.ast, attributes[1]));
}
}
return literals;
}, []);
return (0, utils_js_1.deduplicateLiterals)(literals);
}
function createLiteralsByAngularAst(ctx, ast) {
if (isInterpolation(ast)) {
return ast.expressions.flatMap(expression => {
return createLiteralsByAngularAst(ctx, expression);
});
}
if (isLiteralArray(ast)) {
return ast.expressions.flatMap(expression => {
return createLiteralsByAngularAst(ctx, expression);
});
}
if (isObjectKey(ast)) {
return createLiteralByLiteralMapKey(ctx, ast);
}
if (isConditional(ast)) {
return createLiteralsByAngularConditional(ctx, ast);
}
if (isLiteralPrimitive(ast)) {
return createLiteralByAngularLiteralPrimitive(ctx, ast);
}
if (isTemplateLiteralElement(ast)) {
return createLiteralByAngularTemplateLiteralElement(ctx, ast);
}
return [];
}
function createLiteralsByAngularConditional(ctx, conditional) {
const literals = [];
literals.push(...createLiteralsByAngularAst(ctx, conditional.trueExp));
literals.push(...createLiteralsByAngularAst(ctx, conditional.falseExp));
return literals;
}
function createLiteralsByAngularAttribute(ctx, attribute) {
if (isTextAttribute(attribute)) {
return createLiteralsByAngularTextAttribute(ctx, attribute);
}
if (isBoundAttribute(attribute) && isASTWithSource(attribute.value) && isLiteralPrimitive(attribute.value.ast)) {
return createLiteralsByAngularAst(ctx, attribute.value.ast);
}
return [];
}
function getLiteralsByAngularMatchers(ctx, ast, matchers) {
const matcherFunctions = getAngularMatcherFunctions(ctx, matchers);
const matchingAstNodes = (0, matchers_js_1.getLiteralNodesByMatchers)(ctx, ast, matcherFunctions, value => isAST(value) && isCallExpression(value));
const literals = matchingAstNodes.flatMap(ast => createLiteralsByAngularAst(ctx, ast));
return (0, utils_js_1.deduplicateLiterals)(literals);
}
function getAngularMatcherFunctions(ctx, matchers) {
return matchers.reduce((matcherFunctions, matcher) => {
switch (matcher.match) {
case rule_js_1.MatcherType.String: {
matcherFunctions.push((ast) => {
if (!isAST(ast) ||
isInsideConditionalExpressionCondition(ctx, ast) ||
isInsideLogicalExpressionLeft(ctx, ast) ||
isObjectKey(ast) ||
isObjectValue(ast)) {
return false;
}
return (isStringLiteral(ast) ||
isTemplateLiteralElement(ast) ||
isLiteralArray(ast));
});
break;
}
case rule_js_1.MatcherType.ObjectKey: {
matcherFunctions.push((ast) => {
if (!isAST(ast) ||
!isObjectKey(ast)) {
return false;
}
// objects inside angular templates can not be nested
const path = ast.key;
if (!path || !matcher.pathPattern) {
return true;
}
return (0, matchers_js_1.matchesPathPattern)(path, matcher.pathPattern);
});
break;
}
case rule_js_1.MatcherType.ObjectValue: {
matcherFunctions.push((ast) => {
if (!isAST(ast) ||
!isObjectValue(ast) ||
!hasParent(ast) ||
!isLiteralMap(ast.parent)) {
return false;
}
const index = ast.parent.values.indexOf(ast);
const objectKey = ast.parent.keys[index];
const path = objectKey.key;
if (!path || !matcher.pathPattern) {
return true;
}
return (0, matchers_js_1.matchesPathPattern)(path, matcher.pathPattern);
});
break;
}
}
return matcherFunctions;
}, []);
}
function createLiteralByLiteralMapKey(ctx, key) {
// @ts-expect-error - angular types are faulty
const literalMap = key.parent;
// @ts-expect-error - angular types are faulty
const objectContent = literalMap.parent.source;
const keyContent = key.key;
let start = 0;
let end = 0;
for (const value of literalMap.values) {
const currentStart = objectContent.slice(start).indexOf(keyContent);
const currentEnd = currentStart + keyContent.length;
if (literalMap.sourceSpan.start + currentStart >= value.sourceSpan.start &&
literalMap.sourceSpan.start + currentStart <= value.sourceSpan.end ||
literalMap.sourceSpan.start + currentEnd >= value.sourceSpan.start &&
literalMap.sourceSpan.start + currentEnd <= value.sourceSpan.end) {
start += currentEnd;
end += currentEnd;
continue;
}
start += currentStart - (key.quoted ? 1 : 0);
end += currentEnd + (key.quoted ? 1 : 0);
break;
}
const raw = objectContent.slice(start, end);
const quotes = (0, utils_js_1.getQuotes)(raw);
const whitespaces = (0, utils_js_1.getWhitespace)(keyContent);
const range = [literalMap.sourceSpan.start + start, literalMap.sourceSpan.start + end];
const loc = getLocByRange(ctx, range);
const line = ctx.sourceCode.lines[loc.start.line - 1];
const indentation = (0, utils_js_1.getIndentation)(line);
return [{
...quotes,
...whitespaces,
content: keyContent,
indentation,
loc,
range,
raw,
supportsMultiline: false,
type: "StringLiteral"
}];
}
function createLiteralsByAngularTextAttribute(ctx, attribute) {
const content = attribute.value;
if (!attribute.valueSpan) {
return [];
}
const start = attribute.valueSpan.fullStart;
const end = attribute.valueSpan.end;
const range = [start.offset - 1, end.offset + 1];
const raw = attribute.sourceSpan.start.file.content.slice(...range);
const quotes = (0, utils_js_1.getQuotes)(raw);
const whitespaces = (0, utils_js_1.getWhitespace)(content);
const loc = convertParseSourceSpanToLoc(attribute.valueSpan);
const line = ctx.sourceCode.lines[loc.start.line - 1];
const indentation = (0, utils_js_1.getIndentation)(line);
const supportsMultiline = getMultilineSupport(ctx);
const multilineQuotes = ["'", "\""];
return [{
...quotes,
...whitespaces,
content,
indentation,
loc,
multilineQuotes,
range,
raw,
supportsMultiline,
type: "StringLiteral"
}];
}
function createLiteralByAngularLiteralPrimitive(ctx, literal) {
const content = literal.value;
if (!literal.sourceSpan) {
return [];
}
const start = literal.sourceSpan.start;
const end = literal.sourceSpan.end;
const range = [start, end];
const raw = ctx.sourceCode.text.slice(...range);
const quotes = (0, utils_js_1.getQuotes)(raw);
const whitespaces = (0, utils_js_1.getWhitespace)(content);
const loc = getLocByRange(ctx, range);
const line = ctx.sourceCode.lines[loc.start.line - 1];
const indentation = (0, utils_js_1.getIndentation)(line);
const supportsMultiline = getMultilineSupport(ctx);
const multilineQuotes = supportsMultiline ? ["'", "\"", "`"] : ["'", "\""];
return [{
...quotes,
...whitespaces,
content,
indentation,
loc,
multilineQuotes,
range,
raw,
supportsMultiline,
type: "StringLiteral"
}];
}
function createLiteralByAngularTemplateLiteralElement(ctx, literal) {
const content = literal.text;
if (!literal.sourceSpan || !hasParent(literal)) {
return [];
}
const braces = getBraces(literal);
const start = literal.sourceSpan.start - (braces.closingBraces?.length ?? 0);
const end = literal.sourceSpan.end + (braces.openingBraces?.length ?? 0);
const range = [start, end];
const raw = ctx.sourceCode.text.slice(...range);
const quotes = (0, utils_js_1.getQuotes)(raw);
const whitespaces = (0, utils_js_1.getWhitespace)(content);
const loc = getLocByRange(ctx, range);
const parent = literal.parent;
const parentStart = parent.sourceSpan?.start;
const parentEnd = parent.sourceSpan?.end;
const parentRange = [parentStart, parentEnd];
const parentLoc = getLocByRange(ctx, parentRange);
const parentLine = ctx.sourceCode.lines[parentLoc.start.line - 1];
const indentation = (0, utils_js_1.getIndentation)(parentLine);
const supportsMultiline = getMultilineSupport(ctx);
const multilineQuotes = supportsMultiline ? ["'", "\"", "`"] : ["'", "\""];
return [{
...quotes,
...whitespaces,
...braces,
content,
indentation,
loc,
multilineQuotes,
range,
raw,
supportsMultiline,
type: "TemplateLiteral"
}];
}
function getLocByRange(ctx, range) {
const [rangeStart, rangeEnd] = range;
const loc = {
end: ctx.sourceCode.getLocFromIndex(rangeEnd),
start: ctx.sourceCode.getLocFromIndex(rangeStart)
};
return loc;
}
function convertParseSourceSpanToLoc(sourceSpan) {
return {
end: {
column: sourceSpan.end.col,
line: sourceSpan.end.line + 1
},
start: {
column: sourceSpan.fullStart.col,
line: sourceSpan.fullStart.line + 1
}
};
}
function getMultilineSupport(ctx) {
return !isInsideInlineTemplate(ctx);
}
function isInsideInlineTemplate(ctx) {
return getInlineTemplateComponentIndex(ctx) !== undefined;
}
function getInlineTemplateComponentIndex(ctx) {
const matches = ctx.filename.match(/^.*_inline-template-[\w.-]+-(\d+)\.component\.html$/);
if (matches) {
const [, index] = matches;
return +index;
}
}
function getBraces(literal) {
if (!hasParent(literal)) {
return {};
}
const parent = literal.parent;
const index = parent.elements.indexOf(literal);
if (parent.elements.length === 1) {
return {};
}
return {
closingBraces: index >= 1 ? "}" : undefined,
openingBraces: index < parent.elements.length - 1 ? "${" : undefined
};
}
function getAttributeName(node) {
if (!node.keySpan) {
return node.name;
}
return node.sourceSpan.start.offset !== node.keySpan.start.offset
? node.sourceSpan.fullStart.file.content.slice(node.sourceSpan.start.offset, node.keySpan.end.offset + 1)
: node.keySpan.toString() ?? node.name;
}
function isInsideConditionalExpressionCondition(ctx, ast) {
const parent = findParent(ctx, ast);
if (!parent) {
return false;
}
if (isConditional(parent) && parent.condition === ast) {
return true;
}
return isInsideConditionalExpressionCondition(ctx, parent);
}
function isInsideLogicalExpressionLeft(ctx, ast) {
const parent = findParent(ctx, ast);
if (!parent) {
return false;
}
if (isBinary(parent) && parent.operation === "&&" && parent.left === ast) {
return true;
}
return isInsideConditionalExpressionCondition(ctx, parent);
}
function hasParent(ast) {
return "parent" in ast && ast.parent !== undefined;
}
/**
* The angular parser doesn't provide parent references for all nodes. This function traverses the entire AST
* to find the parent node of the given AST reference.
*
* @param ctx The ESLint rule context.
* @param astNode The AST node to find the parent for.
* @returns The parent AST node, or undefined if not found.
*/
function findParent(ctx, astNode) {
if (hasParent(astNode)) {
return astNode.parent;
}
const ast = ctx.sourceCode.ast;
const visitChildNode = (childNode) => {
if (!childNode || typeof childNode !== "object") {
return;
}
for (const key in childNode) {
if (key === "parent") {
continue;
}
if (childNode[key] === astNode) {
return childNode;
}
const result = visitChildNode(childNode[key]);
if (result) {
return result;
}
}
};
return visitChildNode(ast);
}
function isObjectValue(ast) {
return isStringLiteral(ast) && hasParent(ast) && isLiteralMap(ast.parent);
}
function isObjectKey(ast) {
return "type" in ast && ast.type === "Object" && "key" in ast && ast.key !== undefined;
}
function isStringLiteral(ast) {
return isLiteralPrimitive(ast) && typeof ast.value === "string";
}
function isAST(ast) {
return typeof ast === "object" && ast !== null && "type" in ast;
}
function is(ast, type) {
return "type" in ast && typeof ast.type === "string" && ast.type === type;
}
const isCallExpression = (ast) => is(ast, "Call");
const isASTWithSource = (ast) => is(ast, "ASTWithSource");
const isInterpolation = (ast) => is(ast, "Interpolation");
const isConditional = (ast) => is(ast, "Conditional");
const isBinary = (ast) => is(ast, "Binary");
const isLiteralArray = (ast) => is(ast, "LiteralArray");
const isLiteralMap = (ast) => is(ast, "LiteralMap");
const isTemplateLiteral = (ast) => is(ast, "TemplateLiteral");
const isTemplateLiteralElement = (ast) => is(ast, "TemplateLiteralElement");
const isLiteralPrimitive = (ast) => is(ast, "LiteralPrimitive");
const isTextAttribute = (ast) => is(ast, "TextAttribute");
const isBoundAttribute = (ast) => is(ast, "BoundAttribute");
//# sourceMappingURL=angular.js.map