linter-bundle
Version:
Ready-to use bundle of linting tools, containing configurations for ESLint, stylelint and markdownlint.
301 lines (254 loc) • 6.64 kB
JavaScript
// @ts-nocheck
import stylelint from 'stylelint';
import isOnlyWhitespace from 'stylelint/lib/utils/isOnlyWhitespace.mjs';
import isStandardSyntaxComment from 'stylelint/lib/utils/isStandardSyntaxComment.mjs';
import optionsMatches from 'stylelint/lib/utils/optionsMatches.mjs';
import report from 'stylelint/lib/utils/report.mjs';
import ruleMessages from 'stylelint/lib/utils/ruleMessages.mjs';
import { isAtRule, isComment, isDeclaration, isRule } from 'stylelint/lib/utils/typeGuards.mjs';
import validateOptions from 'stylelint/lib/utils/validateOptions.mjs';
import styleSearch from '../../style-search/index.mjs';
const ruleName = 'plugin/no-eol-whitespace';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected whitespace at end of line'
});
const meta = {
url: 'https://github.com/jens-duttke/linter-bundle/blob/main/stylelint/plugins/stylelint-15.11.0-stylistic/rules/no-eol-whitespace/README.md',
fixable: true
};
const whitespacesToReject = new Set([' ', '\t']);
/**
* @param {string} str
* @returns {string}
*/
function fixString (str) {
return str.replace(/[\t ]+$/u, '');
}
/**
* @param {number} lastEOLIndex
* @param {string} string
* @param {{ ignoreEmptyLines: boolean, isRootFirst: boolean }} options
* @returns {number}
*/
function findErrorStartIndex (lastEOLIndex, string, { ignoreEmptyLines, isRootFirst }) {
const eolWhitespaceIndex = lastEOLIndex - 1;
// If the character before newline is not whitespace, ignore
if (!whitespacesToReject.has(string.charAt(eolWhitespaceIndex))) {
return -1;
}
if (ignoreEmptyLines) {
// If there is only whitespace between the previous newline and
// this newline, ignore
const beforeNewlineIndex = string.lastIndexOf('\n', eolWhitespaceIndex);
if (beforeNewlineIndex >= 0 || isRootFirst) {
const line = string.substring(beforeNewlineIndex, eolWhitespaceIndex);
if (isOnlyWhitespace(line)) {
return -1;
}
}
}
return eolWhitespaceIndex;
}
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary
},
{
optional: true,
actual: secondaryOptions,
possible: {
ignore: ['empty-lines']
}
}
);
if (!validOptions) {
return;
}
const ignoreEmptyLines = optionsMatches(secondaryOptions, 'ignore', 'empty-lines');
const rootString = root.toString();
/**
* @param {number} index
*/
const reportFromIndex = (index) => {
report({
message: messages.rejected,
node: root,
index,
endIndex: index,
result,
ruleName,
fix: () => {
fix(root);
}
});
};
eachEolWhitespace(rootString, reportFromIndex, true);
const errorIndex = findErrorStartIndex(rootString.length, rootString, {
ignoreEmptyLines,
isRootFirst: true
});
if (errorIndex > -1) {
reportFromIndex(errorIndex);
}
/**
* Iterate each whitespace at the end of each line of the given string.
*
* @param {string} string - the source code string
* @param {(index: number) => void} callback - callback the whitespace index at the end of each line
* @param {boolean} isRootFirst - set `true` if the given string is the first token of the root
* @returns {void}
*/
function eachEolWhitespace (string, callback, isRootFirst) {
styleSearch(
{
source: string,
target: ['\n', '\r'],
comments: 'check'
},
(match) => {
const index = findErrorStartIndex(match.startIndex, string, {
ignoreEmptyLines,
isRootFirst
});
if (index > -1) {
callback(index);
}
}
);
}
/**
* @param {import('postcss').Root} rootNode
*/
function fix (rootNode) {
let isRootFirst = true;
rootNode.walk((node) => {
fixText(
node.raws.before,
(fixed) => {
node.raws.before = fixed;
},
isRootFirst
);
isRootFirst = false;
if (isAtRule(node)) {
fixText(node.raws.afterName, (fixed) => {
node.raws.afterName = fixed;
});
const rawsParameters = node.raws.params;
if (rawsParameters) {
fixText(rawsParameters.raw, (fixed) => {
rawsParameters.raw = fixed;
});
}
else {
fixText(node.params, (fixed) => {
node.params = fixed;
});
}
}
if (isRule(node)) {
const rawsSelector = node.raws.selector;
if (rawsSelector) {
fixText(rawsSelector.raw, (fixed) => {
rawsSelector.raw = fixed;
});
}
else {
fixText(node.selector, (fixed) => {
node.selector = fixed;
});
}
}
if (isAtRule(node) || isRule(node) || isDeclaration(node)) {
fixText(node.raws.between, (fixed) => {
node.raws.between = fixed;
});
}
if (isDeclaration(node)) {
const rawsValue = node.raws.value;
if (rawsValue) {
fixText(rawsValue.raw, (fixed) => {
rawsValue.raw = fixed;
});
}
else {
fixText(node.value, (fixed) => {
node.value = fixed;
});
}
}
if (isComment(node)) {
fixText(node.raws.left, (fixed) => {
node.raws.left = fixed;
});
if (!isStandardSyntaxComment(node)) {
node.raws.right = node.raws.right && fixString(node.raws.right);
}
else {
fixText(node.raws.right, (fixed) => {
node.raws.right = fixed;
});
}
fixText(node.text, (fixed) => {
node.text = fixed;
});
}
if (isAtRule(node) || isRule(node)) {
fixText(node.raws.after, (fixed) => {
node.raws.after = fixed;
});
}
});
fixText(
rootNode.raws.after,
(fixed) => {
rootNode.raws.after = fixed;
},
isRootFirst
);
if (typeof rootNode.raws.after === 'string') {
const lastEOL = Math.max(
rootNode.raws.after.lastIndexOf('\n'),
rootNode.raws.after.lastIndexOf('\r')
);
if (lastEOL !== rootNode.raws.after.length - 1) {
rootNode.raws.after =
rootNode.raws.after.slice(0, lastEOL + 1) +
fixString(rootNode.raws.after.slice(lastEOL + 1));
}
}
}
/**
* @param {string | undefined} value
* @param {(text: string) => void} fixFn
* @param {boolean} isRootFirst
*/
function fixText (value, fixFn, isRootFirst = false) {
if (!value) {
return;
}
let fixed = '';
let lastIndex = 0;
eachEolWhitespace(
value,
(index) => {
const newlineIndex = index + 1;
fixed += fixString(value.slice(lastIndex, newlineIndex));
lastIndex = newlineIndex;
},
isRootFirst
);
if (lastIndex) {
fixed += value.slice(lastIndex);
fixFn(fixed);
}
}
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
export default stylelint.createPlugin(ruleName, rule);