298 lines
6.5 KiB
JavaScript
298 lines
6.5 KiB
JavaScript
'use strict';
|
|
|
|
const styleSearch = require('style-search');
|
|
|
|
const isOnlyWhitespace = require('../../utils/isOnlyWhitespace');
|
|
const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
|
|
const optionsMatches = require('../../utils/optionsMatches');
|
|
const report = require('../../utils/report');
|
|
const ruleMessages = require('../../utils/ruleMessages');
|
|
const { isAtRule, isComment, isDeclaration, isRule } = require('../../utils/typeGuards');
|
|
const validateOptions = require('../../utils/validateOptions');
|
|
|
|
const ruleName = 'no-eol-whitespace';
|
|
|
|
const messages = ruleMessages(ruleName, {
|
|
rejected: 'Unexpected whitespace at end of line',
|
|
});
|
|
|
|
const meta = {
|
|
url: 'https://stylelint.io/user-guide/rules/no-eol-whitespace',
|
|
fixable: true,
|
|
deprecated: true,
|
|
};
|
|
|
|
const whitespacesToReject = new Set([' ', '\t']);
|
|
|
|
/**
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
function fixString(str) {
|
|
return str.replace(/[ \t]+$/, '');
|
|
}
|
|
|
|
/**
|
|
* @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, context) => {
|
|
return (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');
|
|
|
|
if (context.fix) {
|
|
fix(root);
|
|
}
|
|
|
|
const rootString = context.fix ? root.toString() : (root.source && root.source.input.css) || '';
|
|
|
|
/**
|
|
* @param {number} index
|
|
*/
|
|
const reportFromIndex = (index) => {
|
|
report({
|
|
message: messages.rejected,
|
|
node: root,
|
|
index,
|
|
result,
|
|
ruleName,
|
|
});
|
|
};
|
|
|
|
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 rawsParams = node.raws.params;
|
|
|
|
if (rawsParams) {
|
|
fixText(rawsParams.raw, (fixed) => {
|
|
rawsParams.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;
|
|
module.exports = rule;
|