| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 | 'use strict';const optionsMatches = require('../../utils/optionsMatches');const report = require('../../utils/report');const ruleMessages = require('../../utils/ruleMessages');const styleSearch = require('style-search');const validateOptions = require('../../utils/validateOptions');const { isNumber } = require('../../utils/validateTypes');const ruleName = 'max-empty-lines';const messages = ruleMessages(ruleName, {	expected: (max) => `Expected no more than ${max} empty ${max === 1 ? 'line' : 'lines'}`,});const meta = {	url: 'https://stylelint.io/user-guide/rules/max-empty-lines',	fixable: true,};/** @type {import('stylelint').Rule} */const rule = (primary, secondaryOptions, context) => {	let emptyLines = 0;	let lastIndex = -1;	return (root, result) => {		const validOptions = validateOptions(			result,			ruleName,			{				actual: primary,				possible: isNumber,			},			{				actual: secondaryOptions,				possible: {					ignore: ['comments'],				},				optional: true,			},		);		if (!validOptions) {			return;		}		const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');		const getChars = replaceEmptyLines.bind(null, primary);		/**		 * 1. walk nodes & replace enterchar		 * 2. deal with special case.		 */		if (context.fix) {			root.walk((node) => {				if (node.type === 'comment' && !ignoreComments) {					node.raws.left = getChars(node.raws.left);					node.raws.right = getChars(node.raws.right);				}				if (node.raws.before) {					node.raws.before = getChars(node.raws.before);				}			});			// first node			const firstNodeRawsBefore = root.first && root.first.raws.before;			// root raws			const rootRawsAfter = root.raws.after;			// not document node			// @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Root'.			if ((root.document && root.document.constructor.name) !== 'Document') {				if (firstNodeRawsBefore) {					root.first.raws.before = getChars(firstNodeRawsBefore, true);				}				if (rootRawsAfter) {					// when max setted 0, should be treated as 1 in this situation.					root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter, true);				}			} else if (rootRawsAfter) {				// `css in js` or `html`				root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter);			}			return;		}		emptyLines = 0;		lastIndex = -1;		const rootString = root.toString();		styleSearch(			{				source: rootString,				target: /\r\n/.test(rootString) ? '\r\n' : '\n',				comments: ignoreComments ? 'skip' : 'check',			},			(match) => {				checkMatch(rootString, match.startIndex, match.endIndex, root);			},		);		/**		 * @param {string} source		 * @param {number} matchStartIndex		 * @param {number} matchEndIndex		 * @param {import('postcss').Root} node		 */		function checkMatch(source, matchStartIndex, matchEndIndex, node) {			const eof = matchEndIndex === source.length;			let problem = false;			// Additional check for beginning of file			if (!matchStartIndex || lastIndex === matchStartIndex) {				emptyLines++;			} else {				emptyLines = 0;			}			lastIndex = matchEndIndex;			if (emptyLines > primary) problem = true;			if (!eof && !problem) return;			if (problem) {				report({					message: messages.expected(primary),					node,					index: matchStartIndex,					result,					ruleName,				});			}			// Additional check for end of file			if (eof && primary) {				emptyLines++;				if (emptyLines > primary && isEofNode(result.root, node)) {					report({						message: messages.expected(primary),						node,						index: matchEndIndex,						result,						ruleName,					});				}			}		}		/**		 * @param {number} maxLines		 * @param {unknown} str		 * @param {boolean?} isSpecialCase		 */		function replaceEmptyLines(maxLines, str, isSpecialCase = false) {			const repeatTimes = isSpecialCase ? maxLines : maxLines + 1;			if (repeatTimes === 0 || typeof str !== 'string') {				return '';			}			const emptyLFLines = '\n'.repeat(repeatTimes);			const emptyCRLFLines = '\r\n'.repeat(repeatTimes);			return /(?:\r\n)+/.test(str)				? str.replace(/(\r\n)+/g, ($1) => {						if ($1.length / 2 > repeatTimes) {							return emptyCRLFLines;						}						return $1;				  })				: str.replace(/(\n)+/g, ($1) => {						if ($1.length > repeatTimes) {							return emptyLFLines;						}						return $1;				  });		}	};};/** * Checks whether the given node is the last node of file. * @param {import('stylelint').PostcssResult['root']} document - the document node with `postcss-html` and `postcss-jsx`. * @param {import('postcss').Root} root - the root node of css */function isEofNode(document, root) {	if (!document || document.constructor.name !== 'Document' || !('type' in document)) {		return true;	}	// In the `postcss-html` and `postcss-jsx` syntax, checks that there is text after the given node.	let after;	if (root === document.last) {		after = document.raws && document.raws.codeAfter;	} else {		// @ts-expect-error -- TS2345: Argument of type 'Root' is not assignable to parameter of type 'number | ChildNode'.		const rootIndex = document.index(root);		const nextNode = document.nodes[rootIndex + 1];		after = nextNode && nextNode.raws && nextNode.raws.codeBefore;	}	return !String(after).trim();}rule.ruleName = ruleName;rule.messages = messages;rule.meta = meta;module.exports = rule;
 |