UNPKG

eslint-plugin-better-tailwindcss

Version:

auto-wraps tailwind classes after a certain print width or class count into multiple lines to improve readability.

463 lines 17.1 kB
import { MatcherType } from "../types/rule.js"; import { getLiteralNodesByMatchers, isAttributesMatchers, isAttributesName, isAttributesRegex, matchesPathPattern } from "../utils/matchers.js"; import { createObjectPathElement, deduplicateLiterals, getIndentation, getQuotes, getWhitespace, matchesName } from "../utils/utils.js"; // https://angular.dev/api/common/NgClass // https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings export function getAttributesByAngularElement(ctx, node) { return [ ...node.attributes, ...node.inputs ]; } export function getLiteralsByAngularAttribute(ctx, attribute, attributes) { const literals = attributes.reduce((literals, attributes) => { if (isAttributesName(attributes)) { if (!matchesName(attributes.toLowerCase(), getAttributeName(attribute).toLowerCase())) { return literals; } literals.push(...createLiteralsByAngularAttribute(ctx, attribute)); } else if (isAttributesRegex(attributes)) { // console.warn("Regex not supported for now"); } else if (isAttributesMatchers(attributes)) { if (!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 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 = getLiteralNodesByMatchers(ctx, ast, matcherFunctions, value => isAST(value) && isCallExpression(value)); const literals = matchingAstNodes.flatMap(ast => createLiteralsByAngularAst(ctx, ast)); return deduplicateLiterals(literals); } function getAngularMatcherFunctions(ctx, matchers) { return matchers.reduce((matcherFunctions, matcher) => { switch (matcher.match) { case MatcherType.String: { matcherFunctions.push((ast) => { if (!isAST(ast) || isInsideConditionalExpressionCondition(ctx, ast) || isInsideLogicalExpressionLeft(ctx, ast) || isObjectKey(ast) || isInsideObjectValue(ctx, ast)) { return false; } return isStringLike(ast); }); break; } case MatcherType.ObjectKey: { matcherFunctions.push((ast) => { if (!isAST(ast) || !isObjectKey(ast) || isInsideConditionalExpressionCondition(ctx, ast) || isInsideLogicalExpressionLeft(ctx, ast)) { return false; } const path = getAngularObjectPath(ctx, ast); if (!path || !matcher.pathPattern) { return true; } return matchesPathPattern(path, matcher.pathPattern); }); break; } case MatcherType.ObjectValue: { matcherFunctions.push((ast) => { if (!isAST(ast) || !hasParent(ast) || !isInsideObjectValue(ctx, ast) || isInsideConditionalExpressionCondition(ctx, ast) || isInsideLogicalExpressionLeft(ctx, ast) || isObjectKey(ast) || !isStringLike(ast)) { return false; } const path = getAngularObjectPath(ctx, ast); if (!path || !matcher.pathPattern) { return true; } return matchesPathPattern(path, matcher.pathPattern); }); break; } } return matcherFunctions; }, []); } function getAngularObjectPath(ctx, ast) { const parent = findParent(ctx, ast); if (!parent) { return; } const paths = []; if (isObjectKey(ast)) { paths.unshift(createObjectPathElement(ast.key)); } if (isLiteralArray(parent)) { const index = parent.expressions.indexOf(ast); paths.unshift(`[${index}]`); } if (isLiteralMap(parent) && isInsideObjectValue(ctx, ast)) { const keyIndex = parent.values.indexOf(ast); const objectKey = parent.keys[keyIndex]; if (objectKey && isObjectKey(objectKey)) { paths.unshift(createObjectPathElement(objectKey.key)); } } paths.unshift(getAngularObjectPath(ctx, parent)); return paths.reduce((paths, currentPath) => { if (!currentPath) { return paths; } if (paths.length === 0) { return [currentPath]; } if (currentPath.startsWith("[") && currentPath.endsWith("]")) { return [...paths, currentPath]; } return [...paths, ".", currentPath]; }, []).join(""); } 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; const keyIndex = literalMap?.keys.indexOf(key); if (keyIndex === undefined || keyIndex === -1) { return []; } const previousValue = literalMap?.values[keyIndex - 1]; const value = literalMap?.values[keyIndex]; if (!literalMap?.sourceSpan || typeof objectContent !== "string" || typeof keyContent !== "string") { return []; } const rangeStart = previousValue?.span?.end ?? 0; const rangeEnd = value?.span?.start ?? objectContent.length; const slice = objectContent.slice(rangeStart, rangeEnd); const start = rangeStart + slice.indexOf(keyContent) - (key.quoted ? 1 : 0); const end = start + keyContent.length + (key.quoted ? 1 : 0); const raw = objectContent.slice(start, end); const quotes = getQuotes(raw); const whitespaces = 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 = 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 = getQuotes(raw); const whitespaces = getWhitespace(content); const loc = convertParseSourceSpanToLoc(attribute.valueSpan); const line = ctx.sourceCode.lines[loc.start.line - 1]; const indentation = 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 = getQuotes(raw); const whitespaces = getWhitespace(content); const loc = getLocByRange(ctx, range); const line = ctx.sourceCode.lines[loc.start.line - 1]; const indentation = 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 = getQuotes(raw); const whitespaces = 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 = 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 isInsideObjectValue(ctx, ast) { const parent = findParent(ctx, ast); if (!parent) { return false; } // #34 allow call expressions as object values if (isCallExpression(ast)) { return false; } if (isObjectValue(ast)) { return true; } if (isLiteralMap(parent) && parent.values.includes(ast)) { return true; } return isInsideObjectValue(ctx, parent); } function isStringLike(ast) { return isStringLiteral(ast) || isTemplateLiteralElement(ast); } 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"; } export 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