eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
261 lines (258 loc) • 8.97 kB
JavaScript
;
const require_runtime = require('../_virtual/_rolldown/runtime.js');
const require_index = require('../utils/index.js');
//#region lib/rules/block-tag-newline.js
/**
* @fileoverview Enforce line breaks style after opening and before closing block-level tags.
* @author Yosuke Ota
*/
var require_block_tag_newline = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
const utils = require_index.default;
/**
* @typedef { 'always' | 'never' | 'consistent' | 'ignore' } OptionType
* @typedef { { singleline?: OptionType, multiline?: OptionType, maxEmptyLines?: number } } ContentsOptions
* @typedef { ContentsOptions & { blocks?: { [element: string]: ContentsOptions } } } Options
* @typedef { Required<ContentsOptions> } ArgsOptions
*/
/**
* @param {string} text Source code as a string.
* @returns {number}
*/
function getLinebreakCount(text) {
return text.split(/\r\n|[\r\n\u2028\u2029]/gu).length - 1;
}
/**
* @param {number} lineBreaks
*/
function getPhrase(lineBreaks) {
switch (lineBreaks) {
case 1: return "1 line break";
default: return `${lineBreaks} line breaks`;
}
}
const ENUM_OPTIONS = { enum: [
"always",
"never",
"consistent",
"ignore"
] };
module.exports = {
meta: {
type: "layout",
docs: {
description: "enforce line breaks after opening and before closing block-level tags",
categories: void 0,
url: "https://eslint.vuejs.org/rules/block-tag-newline.html"
},
fixable: "whitespace",
schema: [{
type: "object",
properties: {
singleline: ENUM_OPTIONS,
multiline: ENUM_OPTIONS,
maxEmptyLines: {
type: "number",
minimum: 0
},
blocks: {
type: "object",
patternProperties: { "^(?:\\S+)$": {
type: "object",
properties: {
singleline: ENUM_OPTIONS,
multiline: ENUM_OPTIONS,
maxEmptyLines: {
type: "number",
minimum: 0
}
},
additionalProperties: false
} },
additionalProperties: false
}
},
additionalProperties: false
}],
messages: {
unexpectedOpeningLinebreak: "There should be no line break after '<{{tag}}>'.",
expectedOpeningLinebreak: "Expected {{expected}} after '<{{tag}}>', but {{actual}} found.",
expectedClosingLinebreak: "Expected {{expected}} before '</{{tag}}>', but {{actual}} found.",
missingOpeningLinebreak: "A line break is required after '<{{tag}}>'.",
missingClosingLinebreak: "A line break is required before '</{{tag}}>'."
}
},
create(context) {
const sourceCode = context.sourceCode;
const df = sourceCode.parserServices.getDocumentFragment && sourceCode.parserServices.getDocumentFragment();
if (!df) return {};
/**
* @param {VStartTag} startTag
* @param {string} beforeText
* @param {number} beforeLinebreakCount
* @param {'always' | 'never'} beforeOption
* @param {number} maxEmptyLines
* @returns {void}
*/
function verifyBeforeSpaces(startTag, beforeText, beforeLinebreakCount, beforeOption, maxEmptyLines) {
if (beforeOption === "always") {
if (beforeLinebreakCount === 0) context.report({
loc: {
start: startTag.loc.end,
end: startTag.loc.end
},
messageId: "missingOpeningLinebreak",
data: { tag: startTag.parent.name },
fix(fixer) {
return fixer.insertTextAfter(startTag, "\n");
}
});
else if (maxEmptyLines < beforeLinebreakCount - 1) context.report({
loc: {
start: startTag.loc.end,
end: sourceCode.getLocFromIndex(startTag.range[1] + beforeText.length)
},
messageId: "expectedOpeningLinebreak",
data: {
tag: startTag.parent.name,
expected: getPhrase(maxEmptyLines + 1),
actual: getPhrase(beforeLinebreakCount)
},
fix(fixer) {
return fixer.replaceTextRange([startTag.range[1], startTag.range[1] + beforeText.length], "\n".repeat(maxEmptyLines + 1));
}
});
} else if (beforeLinebreakCount > 0) context.report({
loc: {
start: startTag.loc.end,
end: sourceCode.getLocFromIndex(startTag.range[1] + beforeText.length)
},
messageId: "unexpectedOpeningLinebreak",
data: { tag: startTag.parent.name },
fix(fixer) {
return fixer.removeRange([startTag.range[1], startTag.range[1] + beforeText.length]);
}
});
}
/**
* @param {VEndTag} endTag
* @param {string} afterText
* @param {number} afterLinebreakCount
* @param {'always' | 'never'} afterOption
* @param {number} maxEmptyLines
* @returns {void}
*/
function verifyAfterSpaces(endTag, afterText, afterLinebreakCount, afterOption, maxEmptyLines) {
if (afterOption === "always") {
if (afterLinebreakCount === 0) context.report({
loc: {
start: endTag.loc.start,
end: endTag.loc.start
},
messageId: "missingClosingLinebreak",
data: { tag: endTag.parent.name },
fix(fixer) {
return fixer.insertTextBefore(endTag, "\n");
}
});
else if (maxEmptyLines < afterLinebreakCount - 1) context.report({
loc: {
start: sourceCode.getLocFromIndex(endTag.range[0] - afterText.length),
end: endTag.loc.start
},
messageId: "expectedClosingLinebreak",
data: {
tag: endTag.parent.name,
expected: getPhrase(maxEmptyLines + 1),
actual: getPhrase(afterLinebreakCount)
},
fix(fixer) {
return fixer.replaceTextRange([endTag.range[0] - afterText.length, endTag.range[0]], "\n".repeat(maxEmptyLines + 1));
}
});
} else if (afterLinebreakCount > 0) context.report({
loc: {
start: sourceCode.getLocFromIndex(endTag.range[0] - afterText.length),
end: endTag.loc.start
},
messageId: "unexpectedOpeningLinebreak",
data: { tag: endTag.parent.name },
fix(fixer) {
return fixer.removeRange([endTag.range[0] - afterText.length, endTag.range[0]]);
}
});
}
/**
* @param {VElement} element
* @param {ArgsOptions} options
* @returns {void}
*/
function verifyElement(element, options) {
const { startTag, endTag } = element;
if (startTag.selfClosing || endTag == null) return;
const text = sourceCode.text.slice(startTag.range[1], endTag.range[0]);
if (!text.trim()) return;
const option = options.multiline !== options.singleline && /[\n\r\u2028\u2029]/u.test(text.trim()) ? options.multiline : options.singleline;
if (option === "ignore") return;
const beforeText = /^\s*/u.exec(text)[0];
const afterText = /\s*$/u.exec(text)[0];
const beforeLinebreakCount = getLinebreakCount(beforeText);
const afterLinebreakCount = getLinebreakCount(afterText);
/** @type {'always' | 'never'} */
let beforeOption;
/** @type {'always' | 'never'} */
let afterOption;
if (option === "always" || option === "never") {
beforeOption = option;
afterOption = option;
} else {
if (beforeLinebreakCount > 0 === afterLinebreakCount > 0) return;
beforeOption = "always";
afterOption = "always";
}
verifyBeforeSpaces(startTag, beforeText, beforeLinebreakCount, beforeOption, options.maxEmptyLines);
verifyAfterSpaces(endTag, afterText, afterLinebreakCount, afterOption, options.maxEmptyLines);
}
/**
* Normalizes a given option value.
* @param { Options | undefined } option An option value to parse.
* @returns { (element: VElement) => void } Verify function.
*/
function normalizeOptionValue(option) {
if (!option) return normalizeOptionValue({});
/** @type {ContentsOptions} */
const contentsOptions = option;
/** @type {ArgsOptions} */
const options = {
singleline: contentsOptions.singleline || "consistent",
multiline: contentsOptions.multiline || "always",
maxEmptyLines: contentsOptions.maxEmptyLines || 0
};
const { blocks } = option;
if (!blocks) return (element) => verifyElement(element, options);
return (element) => {
const { name } = element;
const elementsOptions = blocks[name];
if (elementsOptions) normalizeOptionValue({
singleline: elementsOptions.singleline || options.singleline,
multiline: elementsOptions.multiline || options.multiline,
maxEmptyLines: elementsOptions.maxEmptyLines == null ? options.maxEmptyLines : elementsOptions.maxEmptyLines
})(element);
else verifyElement(element, options);
};
}
const documentFragment = df;
const verify = normalizeOptionValue(context.options[0]);
return utils.defineTemplateBodyVisitor(context, {}, { Program(node) {
if (utils.hasInvalidEOF(node)) return;
for (const element of documentFragment.children) if (utils.isVElement(element)) verify(element);
} });
}
};
}));
//#endregion
Object.defineProperty(exports, 'default', {
enumerable: true,
get: function () {
return require_block_tag_newline();
}
});