| "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; |