UNPKG

eslint-plugin-better-tailwindcss

Version:

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

572 lines 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.enforceConsistentLineWrapping = void 0; const default_options_js_1 = require("../options/default-options.js"); const descriptions_js_1 = require("../options/descriptions.js"); const prefix_js_1 = require("../tailwindcss/prefix.js"); const escape_js_1 = require("../async-utils/escape.js"); const options_js_1 = require("../utils/options.js"); const quotes_js_1 = require("../utils/quotes.js"); const rule_js_1 = require("../utils/rule.js"); const utils_js_1 = require("../utils/utils.js"); const defaultOptions = { attributes: default_options_js_1.DEFAULT_ATTRIBUTE_NAMES, callees: default_options_js_1.DEFAULT_CALLEE_NAMES, classesPerLine: 0, group: "newLine", indent: 2, lineBreakStyle: "unix", preferSingleLine: false, printWidth: 80, tags: default_options_js_1.DEFAULT_TAG_NAMES, variables: default_options_js_1.DEFAULT_VARIABLE_NAMES }; const DOCUMENTATION_URL = "https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/multiline.md"; exports.enforceConsistentLineWrapping = { name: "enforce-consistent-line-wrapping", rule: { create: ctx => (0, rule_js_1.createRuleListener)(ctx, initialize, getOptions, lintLiterals), meta: { docs: { category: "Stylistic Issues", description: "Enforce consistent line wrapping for tailwind classes.", recommended: true, url: DOCUMENTATION_URL }, fixable: "code", schema: [ { additionalProperties: false, properties: { ...descriptions_js_1.CALLEE_SCHEMA, ...descriptions_js_1.ATTRIBUTE_SCHEMA, ...descriptions_js_1.VARIABLE_SCHEMA, ...descriptions_js_1.TAG_SCHEMA, ...descriptions_js_1.ENTRYPOINT_SCHEMA, ...descriptions_js_1.TAILWIND_CONFIG_SCHEMA, ...descriptions_js_1.TSCONFIG_SCHEMA, classesPerLine: { default: defaultOptions.classesPerLine, description: "The maximum amount of classes per line. Lines are wrapped appropriately to stay within this limit . The value `0` disables line wrapping by `classesPerLine`.", type: "integer" }, group: { default: defaultOptions.group, description: "Defines how different groups of classes should be separated. A group is a set of classes that share the same variant.", enum: ["emptyLine", "never", "newLine"], type: "string" }, indent: { default: defaultOptions.indent, description: "Determines how the code should be indented.", oneOf: [ { enum: ["tab"], type: "string" }, { minimum: 0, type: "integer" } ] }, lineBreakStyle: { default: defaultOptions.lineBreakStyle, description: "The line break style. The style `windows` will use `\\r\\n` as line breaks and `unix` will use `\\n`.", enum: ["unix", "windows"], type: "string" }, preferSingleLine: { default: defaultOptions.preferSingleLine, description: "Prefer a single line for the classes. When set to `true`, the rule will keep all classes on a single line until the line exceeds the `printWidth` or `classesPerLine` limit.", type: "boolean" }, printWidth: { default: defaultOptions.printWidth, description: "The maximum line length. Lines are wrapped appropriately to stay within this limit. The value `0` disables line wrapping by `printWidth`.", type: "integer" } }, type: "object" } ], type: "layout" } } }; function initialize() { (0, prefix_js_1.createGetPrefix)(); } function lintLiterals(ctx, literals) { const getPrefix = (0, prefix_js_1.createGetPrefix)(); const options = getOptions(ctx); const { classesPerLine, group: groupSeparator, indent, lineBreakStyle, preferSingleLine, printWidth, tailwindConfig, tsconfig } = options; const { prefix, suffix } = getPrefix({ configPath: tailwindConfig, cwd: ctx.cwd, tsconfigPath: tsconfig }); for (const literal of literals) { if (!literal.supportsMultiline) { continue; } const lineStartPosition = literal.indentation + getIndentation(ctx, indent); const literalStartPosition = literal.loc.start.column; const classChunks = (0, utils_js_1.splitClasses)(literal.content); const groupedClasses = groupClasses(ctx, classChunks, prefix, suffix); const multilineClasses = new Lines(ctx, lineStartPosition); const singlelineClasses = new Lines(ctx, lineStartPosition); if (literal.openingQuote) { if (literal.multilineQuotes?.includes("\\`")) { multilineClasses.line.addMeta({ openingQuote: "\\`" }); } else if (literal.multilineQuotes?.includes("`")) { multilineClasses.line.addMeta({ openingQuote: "`" }); } else { multilineClasses.line.addMeta({ openingQuote: literal.openingQuote }); } } if (literal.openingQuote && literal.closingQuote) { singlelineClasses.line.addMeta({ closingQuote: literal.closingQuote, openingQuote: literal.openingQuote }); } leadingTemplateLiteralNewLine: if (literal.type === "TemplateLiteral" && literal.closingBraces) { multilineClasses.line.addMeta({ closingBraces: literal.closingBraces }); // skip newline for sticky classes if (literal.leadingWhitespace === "" && groupedClasses) { break leadingTemplateLiteralNewLine; } // skip if no classes are present if (!groupedClasses) { break leadingTemplateLiteralNewLine; } if (groupSeparator === "emptyLine") { multilineClasses.addLine(); } if (groupSeparator === "emptyLine" || groupSeparator === "newLine" || groupSeparator === "never") { multilineClasses.addLine(); multilineClasses.line.indent(); } } if (groupedClasses) { for (let g = 0; g < groupedClasses.length; g++) { const group = groupedClasses.at(g); const isFirstGroup = g === 0; if (group.classCount === 0) { continue; } if (isFirstGroup && (literal.type === "TemplateLiteral" && !literal.closingBraces || literal.type !== "TemplateLiteral")) { multilineClasses.addLine(); multilineClasses.line.indent(); } if (!isFirstGroup) { if (groupSeparator === "emptyLine") { multilineClasses.addLine(); } if (groupSeparator === "emptyLine" || groupSeparator === "newLine") { multilineClasses.addLine(); multilineClasses.line.indent(); } } for (let i = 0; i < group.classCount; i++) { const isFirstClass = i === 0; const isLastClass = i === group.classCount - 1; const className = group.at(i); const simulatedLine = multilineClasses.line .clone() .addClass(className) .toString(); // wrap after the first sticky class if (isFirstClass && literal.leadingWhitespace === "" && literal.type === "TemplateLiteral" && literal.closingBraces) { multilineClasses.line.addClass(className); // don't add a new line if the first class is also the last if (isLastClass) { break; } if (groupSeparator === "emptyLine") { multilineClasses.addLine(); } if (groupSeparator === "emptyLine" || groupSeparator === "newLine") { multilineClasses.addLine(); multilineClasses.line.indent(); } continue; } // wrap before the last sticky class if (isLastClass && literal.trailingWhitespace === "" && literal.type === "TemplateLiteral" && literal.openingBraces) { // skip wrapping for the first class of a group if (isFirstClass) { multilineClasses.line.addClass(className); continue; } if (groupSeparator === "emptyLine") { multilineClasses.addLine(); } if (groupSeparator === "emptyLine" || groupSeparator === "newLine") { multilineClasses.addLine(); multilineClasses.line.indent(); } multilineClasses.line.addClass(className); continue; } // wrap if the length exceeds the limits if (simulatedLine.length > printWidth && printWidth !== 0 || multilineClasses.line.classCount >= classesPerLine && classesPerLine !== 0) { // but only if it is not the first class of a group or classes are not grouped if (!isFirstClass || groupSeparator === "never") { multilineClasses.addLine(); multilineClasses.line.indent(); } } multilineClasses.line.addClass(className); singlelineClasses.line.addClass(className); } } } trailingTemplateLiteralNewLine: if (literal.type === "TemplateLiteral" && literal.openingBraces) { // skip newline for sticky classes if (literal.trailingWhitespace === "" && groupedClasses) { multilineClasses.line.addMeta({ openingBraces: literal.openingBraces }); break trailingTemplateLiteralNewLine; } if (groupSeparator === "emptyLine" && groupedClasses) { multilineClasses.addLine(); } if (groupSeparator === "emptyLine" || groupSeparator === "newLine" || groupSeparator === "never") { multilineClasses.addLine(); multilineClasses.line.indent(); } multilineClasses.line.addMeta({ openingBraces: literal.openingBraces }); } if (literal.closingQuote) { multilineClasses.addLine(); multilineClasses.line.indent(lineStartPosition - getIndentation(ctx, indent)); if (literal.multilineQuotes?.includes("\\`")) { multilineClasses.line.addMeta({ closingQuote: "\\`" }); } else if (literal.multilineQuotes?.includes("`")) { multilineClasses.line.addMeta({ closingQuote: "`" }); } else { multilineClasses.line.addMeta({ closingQuote: literal.closingQuote }); } } // collapse lines if there is no reason for line wrapping or if preferSingleLine is enabled collapse: { // disallow collapsing if the literal contains variants, except preferSingleLine is enabled if (groupedClasses?.length !== 1 && !preferSingleLine) { break collapse; } // disallow collapsing for template literals with braces (expressions) if (literal.type === "TemplateLiteral" && (literal.openingBraces || literal.closingBraces)) { break collapse; } // disallow collapsing if the original literal was a single line (keeps original whitespace) if (!literal.content.includes(getLineBreaks(lineBreakStyle))) { break collapse; } // disallow collapsing if the single line contains more classes than the classesPerLine if (singlelineClasses.line.classCount > classesPerLine && classesPerLine !== 0) { break collapse; } // disallow collapsing if the single line including the element and all previous characters is longer than the printWidth if (literalStartPosition + singlelineClasses.line.length > printWidth && printWidth !== 0) { break collapse; } // disallow collapsing if the literal contains expressions if (literal.type === "TemplateLiteral" && (literal.openingBraces || literal.closingBraces)) { break collapse; } const fixedClasses = singlelineClasses.line.toString(false); if (literal.raw === fixedClasses) { continue; } ctx.report({ data: { notReadable: (0, utils_js_1.display)(literal.raw), readable: (0, utils_js_1.display)(fixedClasses) }, fix(fixer) { return fixer.replaceTextRange(literal.range, fixedClasses); }, loc: literal.loc, message: augmentMessage(literal.raw, options, "Unnecessary line wrapping. Expected\n\n{{ notReadable }}\n\nto be\n\n{{ readable }}") }); return; } // skip if class string was empty if (multilineClasses.length === 2) { if (!literal.openingBraces && !literal.closingBraces && literal.content.trim() === "") { continue; } } // skip line wrapping if preferSingleLine is enabled and the single line does not exceed the printWidth or classesPerLine if (preferSingleLine && (literalStartPosition + singlelineClasses.line.length <= printWidth && printWidth !== 0 || singlelineClasses.line.classCount <= classesPerLine && classesPerLine !== 0) || printWidth === 0 && classesPerLine === 0) { continue; } // skip line wrapping if it is not necessary skip: { // disallow skipping if class string contains multiple groups if (groupedClasses && groupedClasses.length > 1) { break skip; } // disallow skipping if the original literal was longer than the printWidth if (literalStartPosition + singlelineClasses.line.length > printWidth && printWidth !== 0 || singlelineClasses.line.classCount > classesPerLine && classesPerLine !== 0) { break skip; } // disallow skipping for template literals with braces (expressions) if (literal.type === "TemplateLiteral" && (literal.openingBraces || literal.closingBraces)) { break skip; } const openingQuoteLength = literal.openingQuote?.length ?? 0; const closingBracesLength = literal.closingBraces?.length ?? 0; const firstLineLength = multilineClasses .at(1) .toString() .trim() .length + openingQuoteLength + closingBracesLength; // disallow skipping if the first line including the element and all previous characters is longer than the printWidth if (literalStartPosition + firstLineLength > printWidth && printWidth !== 0) { break skip; } // disallow skipping if the first line contains more classes than the classesPerLine if (multilineClasses.at(1).classCount > classesPerLine && classesPerLine !== 0) { break skip; } continue; } const fixedClasses = multilineClasses.toString(lineBreakStyle); if (literal.raw === fixedClasses) { continue; } ctx.report({ data: { notReadable: (0, utils_js_1.display)(literal.raw), readable: (0, utils_js_1.display)(fixedClasses) }, fix(fixer) { return literal.surroundingBraces ? fixer.replaceTextRange(literal.range, `{${fixedClasses}}`) : fixer.replaceTextRange(literal.range, fixedClasses); }, loc: literal.loc, message: augmentMessage(literal.raw, options, "Incorrect line wrapping. Expected\n\n{{ notReadable }}\n\nto be\n\n{{ readable }}") }); } } function getIndentation(ctx, indentation) { return indentation === "tab" ? 1 : indentation ?? 0; } function getOptions(ctx) { const options = ctx.options[0] ?? {}; const common = (0, options_js_1.getCommonOptions)(ctx); const printWidth = options.printWidth ?? defaultOptions.printWidth; const classesPerLine = options.classesPerLine ?? defaultOptions.classesPerLine; const indent = options.indent ?? defaultOptions.indent; const group = options.group ?? defaultOptions.group; const preferSingleLine = options.preferSingleLine ?? defaultOptions.preferSingleLine; const lineBreakStyle = options.lineBreakStyle ?? defaultOptions.lineBreakStyle; return { ...common, classesPerLine, group, indent, lineBreakStyle, preferSingleLine, printWidth }; } class Lines { constructor(ctx, indentation) { this.lines = []; this.indentation = 0; this.ctx = ctx; this.indentation = indentation; this.addLine(); } at(index) { return index >= 0 ? this.lines[index] : this.lines[this.lines.length + index]; } get line() { return this.currentLine; } get length() { return this.lines.length; } addLine() { const line = new Line(this.ctx, this.indentation); this.lines.push(line); this.currentLine = line; return this; } toString(lineBreakStyle = "unix") { const lineBreaks = getLineBreaks(lineBreakStyle); return this.lines.map(line => line.toString()).join(lineBreaks); } } class Line { constructor(ctx, indentation) { this.classes = []; this.meta = {}; this.indentation = 0; this.ctx = ctx; this.indentation = indentation; } indent(start = this.indentation) { const indent = getOptions(this.ctx).indent; if (indent === "tab") { this.meta.indentation = "\t".repeat(start); } else { this.meta.indentation = " ".repeat(start); } return this; } get length() { return this.toString().length; } get classCount() { return this.classes.length; } get printWidth() { return this.toString().length; } addMeta(meta) { this.meta = { ...this.meta, ...meta }; return this; } addClass(className) { this.classes.push(className); return this; } clone() { const line = new Line(this.ctx, this.indentation); line.classes = [...this.classes]; line.meta = { ...this.meta }; return line; } toString(indent = true) { return this.join([ indent ? this.meta.indentation : "", this.meta.openingQuote, this.meta.closingBraces, this.meta.leadingWhitespace ?? "", (0, quotes_js_1.escapeNestedQuotes)(this.join(this.classes), this.meta.openingQuote ?? this.meta.closingQuote ?? "`"), this.meta.trailingWhitespace ?? "", this.meta.openingBraces, this.meta.closingQuote ], ""); } join(content, separator = " ") { return content .filter(content => content !== undefined) .join(separator); } } function groupClasses(ctx, classes, prefix, suffix) { if (classes.length === 0) { return; } const prefixRegex = new RegExp(`^${(0, escape_js_1.escapeForRegex)(`${prefix}${suffix}`)}`); const groups = new Groups(); for (const className of classes) { const isFirstClass = classes.indexOf(className) === 0; const isFirstGroup = groups.length === 1; const lastGroup = groups.at(-1); const lastClassName = lastGroup?.at(-1); const unprefixedLastClassName = lastClassName?.replace(prefixRegex, ""); const unprefixedClassName = className.replace(prefixRegex, ""); const lastVariant = unprefixedLastClassName?.match(/^.*?:/)?.[0]; const variant = unprefixedClassName.match(/^.*?:/)?.[0]; if (lastVariant !== variant && !(isFirstClass && isFirstGroup)) { groups.addGroup(); } groups.group.addClass(className); } return groups; } class Groups { constructor() { this.groups = []; this.addGroup(); } get group() { return this.currentGroup; } at(index) { return this.groups.at(index); } get length() { return this.groups.length; } addGroup() { const group = new Group(); this.currentGroup = group; this.groups.push(this.currentGroup); return this; } } class Group { constructor() { this.classes = []; } get classCount() { return this.classes.length; } at(index) { return this.classes.at(index); } addClass(className) { this.classes.push(className); return this; } } function getLineBreaks(lineBreakStyle) { return lineBreakStyle === "unix" ? "\n" : "\r\n"; } function augmentMessage(original, options, message) { const invalidLineBreaks = isLineBreakStyleLikelyMisconfigured(original, options); const invalidIndentations = isIndentationLikelyMisconfigured(original, options); const warnings = []; if (invalidLineBreaks) { warnings.push({ option: "lineBreakStyle", title: "Inconsistent line endings detected", url: `${DOCUMENTATION_URL}#linebreakstyle` }); } if (invalidIndentations) { warnings.push({ option: "indent", title: "Inconsistent indentation detected", url: `${DOCUMENTATION_URL}#indent` }); } return (0, utils_js_1.augmentMessageWithWarnings)(message, DOCUMENTATION_URL, warnings); } function isLineBreakStyleLikelyMisconfigured(original, options) { return (original.includes("\r") && options.lineBreakStyle === "unix" || !original.includes("\r") && options.lineBreakStyle === "windows"); } function isIndentationLikelyMisconfigured(original, options) { return (original.includes(" ") && options.indent === "tab" || original.includes("\t") && typeof options.indent === "number"); } //# sourceMappingURL=enforce-consistent-line-wrapping.js.map