blob: ad2b1b2fcab7e0de8bd8fb472380dbfc6731d0ed [file] [log] [blame]
"use strict";
const isOnlyWhitespace = require("../../utils/isOnlyWhitespace");
const optionsMatches = require("../../utils/optionsMatches");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const styleSearch = require("style-search");
const validateOptions = require("../../utils/validateOptions");
const ruleName = "no-eol-whitespace";
const messages = ruleMessages(ruleName, {
rejected: "Unexpected whitespace at end of line"
});
const whitespacesToReject = new Set([" ", "\t"]);
const rule = function(on, options, context) {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: on
},
{
optional: true,
actual: options,
possible: {
ignore: ["empty-lines"]
}
}
);
if (!validOptions) {
return;
}
if (context.fix) {
fix(root);
}
const rootString = context.fix ? root.toString() : root.source.input.css;
eachEolWhitespace(
rootString,
index => {
report({
message: messages.rejected,
node: root,
index,
result,
ruleName
});
},
true
);
/**
* Iterate each whitespace at the end of each line of the given string.
* @param {string} string the source code string
* @param {Function} 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 eolWhitespaceIndex = match.startIndex - 1;
// If the character before newline is not whitespace, ignore
if (!whitespacesToReject.has(string[eolWhitespaceIndex])) {
return;
}
if (optionsMatches(options, "ignore", "empty-lines")) {
// 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;
}
}
}
callback(eolWhitespaceIndex);
}
);
}
function fix(root) {
let isRootFirst = true;
root.walk(node => {
fixText(
node.raws.before,
fixed => {
node.raws.before = fixed;
},
isRootFirst
);
isRootFirst = false;
// AtRule
fixText(node.raws.afterName, fixed => {
node.raws.afterName = fixed;
});
if (node.raws.params) {
fixText(node.raws.params.raw, fixed => {
node.raws.params.raw = fixed;
});
} else {
fixText(node.params, fixed => {
node.params = fixed;
});
}
// Rule
if (node.raws.selector) {
fixText(node.raws.selector.raw, fixed => {
node.raws.selector.raw = fixed;
});
} else {
fixText(node.selector, fixed => {
node.selector = fixed;
});
}
// AtRule or Rule or Decl
fixText(node.raws.between, fixed => {
node.raws.between = fixed;
});
// Decl
if (node.raws.value) {
fixText(node.raws.value.raw, fixed => {
node.raws.value.raw = fixed;
});
} else {
fixText(node.value, fixed => {
node.value = fixed;
});
}
// Comment
fixText(node.raws.left, fixed => {
node.raws.left = fixed;
});
fixText(node.text, fixed => {
node.text = fixed;
});
//
fixText(node.raws.after, fixed => {
node.raws.after = fixed;
});
});
fixText(
root.raws.after,
fixed => {
root.raws.after = fixed;
},
isRootFirst
);
}
function fixText(value, fix, isRootFirst) {
if (!value) {
return;
}
let fixed = "";
let lastIndex = 0;
eachEolWhitespace(
value,
index => {
const newlineIndex = index + 1;
fixed += value.slice(lastIndex, newlineIndex).replace(/[ \t]+$/, "");
lastIndex = newlineIndex;
},
isRootFirst
);
if (lastIndex) {
fixed += value.slice(lastIndex);
fix(fixed);
}
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;