You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
5.4 KiB
260 lines
5.4 KiB
// @ts-nocheck |
|
|
|
'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']); |
|
|
|
function fixString(str) { |
|
return str.replace(/[ \t]+$/, ''); |
|
} |
|
|
|
function findErrorStartIndex( |
|
lastEOLIndex, |
|
string, |
|
{ ignoreEmptyLines, isRootFirst } = { |
|
ignoreEmptyLines: false, |
|
isRootFirst: false, |
|
}, |
|
) { |
|
const eolWhitespaceIndex = lastEOLIndex - 1; |
|
|
|
// If the character before newline is not whitespace, ignore |
|
if (!whitespacesToReject.has(string[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; |
|
} |
|
|
|
function rule(on, options, context) { |
|
return (root, result) => { |
|
const validOptions = validateOptions( |
|
result, |
|
ruleName, |
|
{ |
|
actual: on, |
|
}, |
|
{ |
|
optional: true, |
|
actual: options, |
|
possible: { |
|
ignore: ['empty-lines'], |
|
}, |
|
}, |
|
); |
|
|
|
if (!validOptions) { |
|
return; |
|
} |
|
|
|
const ignoreEmptyLines = optionsMatches(options, 'ignore', 'empty-lines'); |
|
|
|
if (context.fix) { |
|
fix(root); |
|
} |
|
|
|
const rootString = context.fix ? root.toString() : root.source.input.css; |
|
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 {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 index = findErrorStartIndex(match.startIndex, string, { |
|
ignoreEmptyLines, |
|
isRootFirst, |
|
}); |
|
|
|
if (index > -1) { |
|
callback(index); |
|
} |
|
}, |
|
); |
|
} |
|
|
|
function fix(rootNode) { |
|
let isRootFirst = true; |
|
|
|
rootNode.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; |
|
}); |
|
|
|
if (node.raws.inline) { |
|
node.raws.right = fixString(node.raws.right); |
|
} else { |
|
fixText(node.raws.right, (fixed) => { |
|
node.raws.right = fixed; |
|
}); |
|
} |
|
|
|
fixText(node.text, (fixed) => { |
|
node.text = fixed; |
|
}); |
|
|
|
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)); |
|
} |
|
} |
|
} |
|
|
|
function fixText(value, fixFn, isRootFirst) { |
|
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; |
|
module.exports = rule;
|
|
|