@html-eslint/eslint-plugin
Version:
ESLint plugin for html
196 lines (180 loc) • 4.93 kB
JavaScript
/**
* @typedef { import("../types").RuleFixer } RuleFixer
* @typedef { import("@html-eslint/types").Attribute } Attribute
* @typedef { import("@html-eslint/types").Text } Text
*
* @typedef {Object} Option
* @property {string[]} [Option.priority]
* @typedef { import("../types").RuleModule<[Option]> } RuleModule
*/
const { hasTemplate } = require("./utils/node");
const { RULE_CATEGORY } = require("../constants");
const { getSourceCode } = require("./utils/source-code");
const { createVisitors } = require("./utils/visitors");
const MESSAGE_IDS = {
UNSORTED: "unsorted",
};
/**
* @type {RuleModule}
*/
module.exports = {
meta: {
type: "code",
docs: {
description: "Enforce attributes alphabetical sorting",
category: RULE_CATEGORY.STYLE,
recommended: false,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
priority: {
type: "array",
items: {
type: "string",
uniqueItems: true,
},
},
},
},
],
messages: {
[MESSAGE_IDS.UNSORTED]: "Attributes should be sorted alphabetically",
},
},
create(context) {
const sourceCode = getSourceCode(context);
const option = context.options[0] || {
priority: ["id", "type", "class", "style"],
};
/**
* @type {string[]}
*/
const priority = option.priority || [];
/**
* @param {Attribute} attrA
* @param {Attribute} attrB
* @return {number}
*/
function compare(attrA, attrB) {
const keyA = attrA.key.value;
const keyB = attrB.key.value;
const keyAReservedValue = priority.indexOf(keyA);
const keyBReservedValue = priority.indexOf(keyB);
if (keyAReservedValue >= 0 && keyBReservedValue >= 0) {
return keyAReservedValue - keyBReservedValue;
} else if (keyAReservedValue >= 0) {
return -1;
} else if (keyBReservedValue >= 0) {
return 1;
}
return keyA.localeCompare(keyB);
}
/**
* @param {string} source
* @param {Attribute[]} unsorted
* @param {Attribute[]} sorted
* @returns {string}
*/
function getSortedCode(source, unsorted, sorted) {
let result = "";
unsorted.forEach((unsortedAttr, index) => {
const sortedAttr = sorted[index];
result += source.slice(sortedAttr.range[0], sortedAttr.range[1]);
const nextUnsortedAttr = unsorted[index + 1];
if (nextUnsortedAttr) {
result += source.slice(
unsortedAttr.range[1],
nextUnsortedAttr.range[0]
);
}
});
return result;
}
/**
* @param {RuleFixer} fixer
* @param {Attribute[]} unsorted
* @param {Attribute[]} sorted
*/
function fix(fixer, unsorted, sorted) {
const source = sourceCode.getText();
return fixer.replaceTextRange(
[unsorted[0].range[0], unsorted[unsorted.length - 1].range[1]],
getSortedCode(source, unsorted, sorted)
);
}
/**
* @param {Attribute[]} before
* @param {Attribute[]} after
* @returns {boolean}
*/
function isChanged(before, after) {
for (let i = 0; i < before.length; i++) {
if (before[i] !== after[i]) return true;
}
return false;
}
/**
* @param {Attribute[]} attributes
* @return {Attribute[][]}
*/
function groupAttributes(attributes) {
/**
* @type {Attribute[][]}
*/
const attributesList = [];
let index = 0;
for (const attribute of attributes) {
if (hasTemplate(attribute.key)) {
index = attributesList.length;
continue;
}
if (!attributesList[index]) {
attributesList[index] = [];
}
attributesList[index].push(attribute);
}
return attributesList;
}
/**
* @param {Attribute[]} unsorted
*/
function checkSorting(unsorted) {
if (unsorted.length <= 1) {
return;
}
const grouped = groupAttributes(unsorted);
grouped.forEach((unsorted) => {
const sorted = [...unsorted].sort(compare);
if (!isChanged(unsorted, sorted)) {
return;
}
const first = unsorted[0];
const last = unsorted[unsorted.length - 1];
context.report({
loc: {
start: first.loc.start,
end: last.loc.end,
},
messageId: MESSAGE_IDS.UNSORTED,
fix(fixer) {
return fix(fixer, unsorted, sorted);
},
});
});
}
return createVisitors(context, {
ScriptTag(node) {
checkSorting(node.attributes);
},
Tag(node) {
checkSorting(node.attributes);
},
StyleTag(node) {
checkSorting(node.attributes);
},
});
},
};