UNPKG

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
"use strict"; 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