| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 | 'use strict';const stripIndent = require('strip-indent');const indentString = require('indent-string');const esquery = require('esquery');const {replaceTemplateElement} = require('./fix/index.js');const {callExpressionSelector, methodCallSelector} = require('./selectors/index.js');const MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE = 'template-indent';const messages = {	[MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE]: 'Templates should be properly indented.',};const jestInlineSnapshotSelector = [	callExpressionSelector({name: 'expect', path: 'callee.object', argumentsLength: 1}),	methodCallSelector({method: 'toMatchInlineSnapshot', argumentsLength: 1}),	' > TemplateLiteral.arguments:first-child',].join('');/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const sourceCode = context.getSourceCode();	const options = {		tags: ['outdent', 'dedent', 'gql', 'sql', 'html', 'styled'],		functions: ['dedent', 'stripIndent'],		selectors: [jestInlineSnapshotSelector],		comments: ['HTML', 'indent'],		...context.options[0],	};	options.comments = options.comments.map(comment => comment.toLowerCase());	const selectors = [		...options.tags.map(tagName => `TaggedTemplateExpression[tag.name="${tagName}"] > .quasi`),		...options.functions.map(functionName => `CallExpression[callee.name="${functionName}"] > .arguments`),		...options.selectors,	];	/** @param {import('@babel/core').types.TemplateLiteral} node */	const indentTemplateLiteralNode = node => {		const delimiter = '__PLACEHOLDER__' + Math.random();		const joined = node.quasis			.map(quasi => {				const untrimmedText = sourceCode.getText(quasi);				return untrimmedText.slice(1, quasi.tail ? -1 : -2);			})			.join(delimiter);		const eolMatch = joined.match(/\r?\n/);		if (!eolMatch) {			return;		}		const eol = eolMatch[0];		const startLine = sourceCode.lines[node.loc.start.line - 1];		const marginMatch = startLine.match(/^(\s*)\S/);		const parentMargin = marginMatch ? marginMatch[1] : '';		let indent;		if (typeof options.indent === 'string') {			indent = options.indent;		} else if (typeof options.indent === 'number') {			indent = ' '.repeat(options.indent);		} else {			const tabs = parentMargin.startsWith('\t');			indent = tabs ? '\t' : '  ';		}		const dedented = stripIndent(joined);		const trimmed = dedented.replace(new RegExp(`^${eol}|${eol}[ \t]*$`, 'g'), '');		const fixed			= eol			+ indentString(trimmed, 1, {indent: parentMargin + indent})			+ eol			+ parentMargin;		if (fixed === joined) {			return;		}		context.report({			node,			messageId: MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE,			fix: fixer => fixed				.split(delimiter)				.map((replacement, index) => replaceTemplateElement(fixer, node.quasis[index], replacement)),		});	};	return {		/** @param {import('@babel/core').types.TemplateLiteral} node */		TemplateLiteral(node) {			if (options.comments.length > 0) {				const previousToken = sourceCode.getTokenBefore(node, {includeComments: true});				if (previousToken?.type === 'Block' && options.comments.includes(previousToken.value.trim().toLowerCase())) {					indentTemplateLiteralNode(node);					return;				}			}			const ancestry = context.getAncestors().reverse();			const shouldIndent = selectors.some(selector => esquery.matches(node, esquery.parse(selector), ancestry));			if (shouldIndent) {				indentTemplateLiteralNode(node);			}		},	};};/** @type {import('json-schema').JSONSchema7[]} */const schema = [	{		type: 'object',		additionalProperties: false,		properties: {			indent: {				oneOf: [					{						type: 'string',						pattern: /^\s+$/.source,					},					{						type: 'integer',						minimum: 1,					},				],			},			tags: {				type: 'array',				uniqueItems: true,				items: {					type: 'string',				},			},			functions: {				type: 'array',				uniqueItems: true,				items: {					type: 'string',				},			},			selectors: {				type: 'array',				uniqueItems: true,				items: {					type: 'string',				},			},			comments: {				type: 'array',				uniqueItems: true,				items: {					type: 'string',				},			},		},	},];/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Fix whitespace-insensitive template indentation.',		},		fixable: 'code',		schema,		messages,	},};
 |