UNPKG

@html-eslint/eslint-plugin

Version:
319 lines (292 loc) 6.58 kB
/** * @import { * AnyNode, * AnyToken, * Attribute, * AttributeKey, * AttributeValue, * CloseTemplate, * Comment, * CommentContent, * OpenTemplate, * ScriptTag, * StyleTag, * Tag, * Text * } from "@html-eslint/types" * @import {AST} from "eslint" * @import { * BaseNode, * Line * } from "../../types" */ const { NODE_TYPES } = require("@html-eslint/parser"); /** * @param {Tag | ScriptTag | StyleTag} node * @param {string} key * @returns {Attribute | undefined} */ function findAttr(node, key) { return node.attributes.find( (attr) => attr.key && attr.key.value.toLowerCase() === key.toLowerCase() ); } /** * @param {Tag | ScriptTag} node * @param {string} attrName * @returns {boolean} */ function hasAttr(node, attrName) { return node.attributes.some( (a) => a.type === "Attribute" && a.key.value === attrName ); } /** * Checks whether a node's attributes is empty or not. * * @param {Tag | ScriptTag | StyleTag} node * @returns {boolean} */ function isAttributesEmpty(node) { return !node.attributes || node.attributes.length <= 0; } /** * Checks whether a node's all tokens are on the same line or not. * * @param {AnyNode} node A node to check * @returns {boolean} `true` if a node's tokens are on the same line, otherwise * `false`. */ function isNodeTokensOnSameLine(node) { return node.loc.start.line === node.loc.end.line; } /** * @param {AST.Range} rangeA * @param {AST.Range} rangeB * @returns {boolean} */ function isRangesOverlap(rangeA, rangeB) { return rangeA[0] < rangeB[1] && rangeB[0] < rangeA[1]; } /** * @param {(Text | CommentContent)["parts"]} parts * @param {AST.Range} range * @returns {boolean} */ function isOverlapWithTemplates(parts, range) { return parts .filter((part) => part.type !== NODE_TYPES.Part) .some((part) => isRangesOverlap(part.range, range)); } /** * @param {AttributeKey | AttributeValue | Text | CommentContent} node * @returns {boolean} */ function hasTemplate(node) { return node.parts.some((part) => part.type !== NODE_TYPES.Part); } /** * @param {Text | CommentContent} node * @returns {Line[]} */ function splitToLineNodes(node) { let start = node.range[0]; let line = node.loc.start.line; const startCol = node.loc.start.column; /** @type {Line[]} */ const lineNodes = []; const parts = node.parts || []; /** * @param {AST.Range} range * @returns {{ * isOverlapTemplate: boolean; * hasOpenTemplate: boolean; * hasCloseTemplate: boolean; * }} */ function getTemplateInfo(range) { let isOverlapTemplate = false; let hasOpenTemplate = false; let hasCloseTemplate = false; for (const part of parts) { if (part.type === NODE_TYPES.Template) { if (isRangesOverlap(part.range, range)) { isOverlapTemplate = true; } if (part.open && isRangesOverlap(part.open.range, range)) { hasOpenTemplate = true; } if (part.close && isRangesOverlap(part.close.range, range)) { hasCloseTemplate = true; } } } return { isOverlapTemplate, hasCloseTemplate, hasOpenTemplate, }; } node.value.split("\n").forEach((value, index) => { const columnStart = index === 0 ? startCol : 0; /** @type {AST.Range} */ const range = [start, start + value.length]; const loc = { start: { line, column: columnStart, }, end: { line, column: columnStart + value.length, }, }; /** @type {Line} */ const lineNode = { type: "Line", value, range, loc, ...getTemplateInfo(range), }; start += value.length + 1; line += 1; lineNodes.push(lineNode); }); return lineNodes; } /** * Get location between two nodes. * * @param {{ loc: AST.SourceLocation }} before A node placed in before * @param {{ loc: AST.SourceLocation }} after A node placed in after * @returns {AST.SourceLocation} Location between two nodes. */ function getLocBetween(before, after) { return { start: before.loc.end, end: after.loc.start, }; } /** * @param {BaseNode} node * @returns {node is Tag} */ function isTag(node) { return node.type === NODE_TYPES.Tag; } /** * @param {BaseNode} node * @returns {node is ScriptTag} */ function isScript(node) { return node.type === NODE_TYPES.ScriptTag; } /** * @param {BaseNode} node * @returns {node is StyleTag} */ function isStyle(node) { return node.type === NODE_TYPES.StyleTag; } /** * @param {BaseNode} node * @returns {node is Comment} */ function isComment(node) { return node.type === NODE_TYPES.Comment; } /** * @param {BaseNode} node * @returns {node is Text} */ function isText(node) { return node.type === NODE_TYPES.Text; } /** * @param {BaseNode} node * @returns {node is Line} */ function isLine(node) { return node.type === "Line"; } const lineBreakPattern = /\r\n|[\r\n\u2028\u2029]/u; const lineEndingPattern = new RegExp(lineBreakPattern.source, "gu"); /** * @param {string} source * @returns {string[]} */ function codeToLines(source) { return source.split(lineEndingPattern); } /** * @param {AnyNode} node * @param {(node: AnyNode) => boolean} predicate * @returns {null | AnyNode} */ function findParent(node, predicate) { if (!node.parent) { return null; } if ( node.type === "TaggedTemplateExpression" || node.type === "TemplateLiteral" || node.type === "TemplateElement" ) { return null; } if (predicate(node.parent)) { return node.parent; } return findParent(node.parent, predicate); } /** * @param {AnyToken[]} tokens * @returns {(CommentContent | Text)["parts"][number][]} */ function getTemplateTokens(tokens) { return ( [] .concat( ...tokens // @ts-ignore .map((token) => token["parts"] || []) ) // @ts-ignore .filter((token) => token.type !== NODE_TYPES.Part) ); } /** * @param {Tag | ScriptTag | StyleTag} node * @returns {string} */ function getNameOf(node) { if (isScript(node)) { return "script"; } if (isStyle(node)) { return "style"; } return node.name.toLowerCase(); } module.exports = { findAttr, isAttributesEmpty, isNodeTokensOnSameLine, splitToLineNodes, getLocBetween, findParent, isTag, isComment, isText, isLine, isScript, isStyle, isOverlapWithTemplates, codeToLines, isRangesOverlap, getTemplateTokens, hasTemplate, hasAttr, getNameOf, };