UNPKG

@yusufkandemir/eslint-plugin-lodash-template

Version:

ESLint plugin for John Resig-style micro template, Lodash's template, Underscore's template and EJS.

255 lines (239 loc) 9.82 kB
"use strict"; const utils = require("../utils"); const { getSourceCode } = require("eslint-compat-utils"); /** * Check whether the given node is a multiline * @param {object} contentFirstLoc The comment content first location. * @param {object} contentLastLoc The comment content last location. * @returns {boolean} `true` if the node is a multiline. */ function isMultiline(contentFirstLoc, contentLastLoc) { if (contentFirstLoc.line < contentLastLoc.line) { // multiline contents return true; } return false; } /** * Normalize options. * @param {object} options The options user configured. * @returns {object} The normalized options. */ function parseOptions(options) { return Object.assign( { singleline: "never", multiline: "always", }, options, ); } /** * Get the messageId for before tag open * @param {number} expectedLineBreaks The number of expected line breaks * @param {number} actualLineBreaks The number of actual line breaks * @returns {string} The messageId */ function getMessageIdForTagOpen(expectedLineBreaks, actualLineBreaks) { if (expectedLineBreaks === 0) { if (actualLineBreaks === 1) { return "unexpectedAfterTagOpen"; } return "expectedNoLineBreaksAfterTagOpenButNLineBreaks"; } if (actualLineBreaks === 0) { return "missingAfterTagOpen"; } return "expectedOneLineBreakAfterTagOpenButNLineBreaks"; } /** * Get the messageId for after tag close * @param {number} expectedLineBreaks The number of expected line breaks * @param {number} actualLineBreaks The number of actual line breaks * @returns {string} The messageId */ function getMessageIdForTagClose(expectedLineBreaks, actualLineBreaks) { if (expectedLineBreaks === 0) { if (actualLineBreaks === 1) { return "unexpectedBeforeTagClose"; } return "expectedNoLineBreaksBeforeTagCloseButNLineBreaks"; } if (actualLineBreaks === 0) { return "missingBeforeTagClose"; } return "expectedOneLineBreakBeforeTagCloseButNLineBreaks"; } module.exports = { meta: { docs: { description: "require or disallow a line break before and after HTML comment contents", category: "recommended-with-html", url: "https://ota-meshi.github.io/eslint-plugin-lodash-template/rules/html-comment-content-newline.html", }, fixable: "whitespace", messages: { unexpectedAfterTagOpen: "Expected no line breaks after `<!--`, but 1 line break found.", expectedNoLineBreaksAfterTagOpenButNLineBreaks: "Expected no line breaks after `<!--`, but {{n}} line breaks found.", missingAfterTagOpen: "Expected 1 line break after `<!--`, but no line breaks found.", expectedOneLineBreakAfterTagOpenButNLineBreaks: "Expected 1 line break after `<!--`, but {{n}} line breaks found.", unexpectedBeforeTagClose: "Expected no line breaks before `-->`, but 1 line break found.", expectedNoLineBreaksBeforeTagCloseButNLineBreaks: "Expected no line breaks before `-->`, but {{n}} line breaks found.", missingBeforeTagClose: "Expected 1 line break before `-->`, but no line breaks found.", expectedOneLineBreakBeforeTagCloseButNLineBreaks: "Expected 1 line break before `-->`, but {{n}} line breaks found.", }, schema: [ { type: "object", properties: { singleline: { enum: ["ignore", "always", "never"] }, multiline: { enum: ["ignore", "always", "never"] }, }, additionalProperties: false, }, ], type: "layout", }, create(context) { const sourceCode = getSourceCode(context); if (!sourceCode.parserServices.getMicroTemplateService) { return {}; } if (!utils.isHtmlFile(context.getFilename())) { return {}; } const microTemplateService = sourceCode.parserServices.getMicroTemplateService(); const options = parseOptions(context.options[0]); /** * Get the comments locations. * @param {ASTNode} node The HTML comment. * @returns {object} The comments locations. */ function getCommentLocations(node) { const contentStartIndex = node.commentOpen.range[1]; const contentEndIndex = node.commentClose.range[0]; const contentText = sourceCode.text.slice( contentStartIndex, contentEndIndex, ); const contentFirstIndex = (() => { const index = contentText.search(/\S/u); if (index >= 0) { return index + contentStartIndex; } return contentEndIndex; })(); const contentLastIndex = (() => { const index = contentText.search(/\S\s*$/gu); if (index >= 0) { return index + contentStartIndex + 1; } return contentStartIndex; })(); return { first: { index: contentFirstIndex, loc: sourceCode.getLocFromIndex(contentFirstIndex), }, last: { index: contentLastIndex, loc: sourceCode.getLocFromIndex(contentLastIndex), }, }; } return { "Program:exit"() { microTemplateService.traverseDocumentNodes({ HTMLComment(node) { const commentOpen = node.commentOpen; const commentClose = node.commentClose; if (!commentOpen || !commentClose) { return; } const commentLocs = getCommentLocations(node); const type = isMultiline( commentLocs.first.loc, commentLocs.last.loc, ) ? options.multiline : options.singleline; if (type === "ignore") { // 'ignore' option return; } const beforeLineBreaks = commentLocs.first.loc.line - commentOpen.loc.end.line; const afterLineBreaks = commentClose.loc.start.line - commentLocs.last.loc.line; const expectedLineBreaks = type === "always" ? 1 : 0; if (expectedLineBreaks !== beforeLineBreaks) { context.report({ loc: { start: commentOpen.loc.end, end: commentLocs.first.loc, }, messageId: getMessageIdForTagOpen( expectedLineBreaks, beforeLineBreaks, ), data: { n: beforeLineBreaks, }, fix(fixer) { const range = [ commentOpen.range[1], commentLocs.first.index, ]; const text = "\n".repeat( expectedLineBreaks, ); return fixer.replaceTextRange(range, text); }, }); } if (commentOpen.range[1] === commentLocs.last.index) { return; } if (expectedLineBreaks !== afterLineBreaks) { context.report({ loc: { start: commentLocs.last.loc, end: commentClose.loc.start, }, messageId: getMessageIdForTagClose( expectedLineBreaks, afterLineBreaks, ), data: { n: afterLineBreaks, }, fix(fixer) { const range = [ commentLocs.last.index, commentClose.range[0], ]; const text = "\n".repeat( expectedLineBreaks, ); return fixer.replaceTextRange(range, text); }, }); } }, }); }, }; }, };