UNPKG

@html-eslint/eslint-plugin

Version:
152 lines (137 loc) 3.54 kB
/** * @import { * Comment, * CommentContent, * Tag, * Text * } from "@html-eslint/types" * @import {RuleModule} from "../types" * @typedef {Object} Option * @property {string[]} [Option.skip] */ const { RULE_CATEGORY } = require("../constants"); const { isTag, isOverlapWithTemplates } = require("./utils/node"); const { getSourceCode } = require("./utils/source-code"); const { createVisitors } = require("./utils/visitors"); const { getRuleUrl } = require("./utils/rule"); const MESSAGE_IDS = { UNEXPECTED: "unexpected", }; /** @type {RuleModule<[Option]>} */ module.exports = { meta: { type: "code", docs: { description: "Disallow unnecessary consecutive spaces", category: RULE_CATEGORY.STYLE, recommended: false, url: getRuleUrl("no-extra-spacing-text"), }, fixable: true, schema: [ { type: "object", properties: { skip: { type: "array", items: { type: "string", }, }, }, additionalProperties: false, }, ], messages: { [MESSAGE_IDS.UNEXPECTED]: "Tabs and/or multiple consecutive spaces not allowed here", }, }, create(context) { const options = context.options[0] || {}; /** @type {string[]} */ const skipTags = options.skip || []; const sourceCode = getSourceCode(context); /** @type {Tag[]} */ const tagStack = []; /** * @param {Comment | Text} node * @returns {boolean} */ function hasSkipTagOnParent(node) { // @ts-ignore const parent = node.parent; if ( parent && // @ts-ignore isTag(parent) && skipTags.some((skipTag) => skipTag === parent.name) ) { return true; } return false; } /** @param {CommentContent | Text} node */ function stripConsecutiveSpaces(node) { const text = node.value; const matcher = /(^|[^\n \t])([ \t]+\n|\t[\t ]*|[ \t]{2,})/g; while (true) { const offender = matcher.exec(text); if (offender === null) { break; } const space = offender[2]; const indexStart = node.range[0] + matcher.lastIndex - space.length; const indexEnd = indexStart + space.length; const hasOverlap = isOverlapWithTemplates(node.parts, [ indexStart, indexEnd, ]); if (hasOverlap) { return; } context.report({ node: node, loc: { start: sourceCode.getLocFromIndex(indexStart), end: sourceCode.getLocFromIndex(indexEnd), }, messageId: MESSAGE_IDS.UNEXPECTED, fix(fixer) { return fixer.replaceTextRange( [indexStart, indexEnd], space.endsWith(`\n`) ? `\n` : ` ` ); }, }); } } return createVisitors(context, { Comment(node) { if (hasSkipTagOnParent(node)) { return; } stripConsecutiveSpaces(node.value); }, Text(node) { if (hasSkipTagOnParent(node)) { return; } stripConsecutiveSpaces(node); }, Tag(node) { tagStack.push(node); if ( skipTags.some((skipTag) => tagStack.some((tag) => tag.name === skipTag) ) ) { return; } }, "Tag:exit"() { tagStack.pop(); }, }); }, };