@stylistic/stylelint-plugin
Version:
A collection of stylistic/formatting Stylelint rules
288 lines (239 loc) • 6.95 kB
JavaScript
import styleSearch from "style-search"
import stylelint from "stylelint"
import { addNamespace } from "../../utils/addNamespace/index.js"
import { getRuleDocUrl } from "../../utils/getRuleDocUrl/index.js"
import { isOnlyWhitespace } from "../../utils/isOnlyWhitespace/index.js"
import { isStandardSyntaxComment } from "../../utils/isStandardSyntaxComment/index.js"
import { optionsMatches } from "../../utils/optionsMatches/index.js"
import { isAtRule, isComment, isDeclaration, isRule } from "../../utils/typeGuards/index.js"
let { utils: { report, ruleMessages, validateOptions } } = stylelint
let shortName = `no-eol-whitespace`
export let ruleName = addNamespace(shortName)
export let messages = ruleMessages(ruleName, {
rejected: `Unexpected whitespace at end of line`,
})
export let meta = {
url: getRuleDocUrl(shortName),
fixable: true,
}
let whitespacesToReject = new Set([` `, `\t`])
/**
* Fixes whitespace at the end of a string.
* @param {string} str - The string to fix.
* @returns {string} The fixed string.
*/
function fixString (str) {
return str.replace(/[ \t]+$/u, ``)
}
/**
* Finds the start index of EOL whitespace error.
* @param {number} lastEOLIndex - The last end-of-line index.
* @param {string} string - The source code string.
* @param {{ ignoreEmptyLines: boolean, isRootFirst: boolean }} options - The options object
* @returns {number} The error start index, or -1 if no error.
*/
function findErrorStartIndex (lastEOLIndex, string, { ignoreEmptyLines, isRootFirst }) {
let 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
let beforeNewlineIndex = string.lastIndexOf(`\n`, eolWhitespaceIndex)
if (beforeNewlineIndex >= 0 || isRootFirst) {
let line = string.slice(Math.max(0, beforeNewlineIndex), eolWhitespaceIndex)
if (isOnlyWhitespace(line)) return -1
}
}
return eolWhitespaceIndex
}
/**
* Disallows end-of-line whitespace.
* @type {import('stylelint').Rule}
*/
function rule (primary, secondaryOptions) {
return (root, result) => {
let validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
},
{
optional: true,
actual: secondaryOptions,
possible: {
ignore: [`empty-lines`],
},
},
)
if (!validOptions) return
let ignoreEmptyLines = optionsMatches(secondaryOptions, `ignore`, `empty-lines`)
let rootString = (root.source && root.source.input.css) || ``
/**
* Reports EOL whitespace violations from the specified index.
* @param {number} index - The index to report from.
*/
function reportFromIndex (index) {
report({
message: messages.rejected,
node: root,
index,
endIndex: index,
result,
ruleName,
fix,
})
}
eachEolWhitespace(rootString, reportFromIndex, true)
let 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) => {
let index = findErrorStartIndex(match.startIndex, string, {
ignoreEmptyLines,
isRootFirst,
})
if (index > -1) callback(index)
},
)
}
function fix () {
let isRootFirst = true
root.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
})
let rawsParams = node.raws.params
if (rawsParams) {
fixText(rawsParams.raw, (fixed) => {
rawsParams.raw = fixed
})
}
else {
fixText(node.params, (fixed) => {
node.params = fixed
})
}
}
if (isRule(node)) {
let 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)) {
let 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)) {
fixText(node.raws.right, (fixed) => {
node.raws.right = fixed
})
}
else node.raws.right = node.raws.right && fixString(node.raws.right)
fixText(node.text, (fixed) => {
node.text = fixed
})
}
if (isAtRule(node) || isRule(node)) {
fixText(node.raws.after, (fixed) => {
node.raws.after = fixed
})
}
})
fixText(
root.raws.after,
(fixed) => {
root.raws.after = fixed
},
isRootFirst,
)
if (typeof root.raws.after === `string`) {
let lastEOL = Math.max(
root.raws.after.lastIndexOf(`\n`),
root.raws.after.lastIndexOf(`\r`),
)
if (lastEOL !== root.raws.after.length - 1) root.raws.after = root.raws.after.slice(0, lastEOL + 1) + fixString(root.raws.after.slice(lastEOL + 1))
}
}
/**
* Fixes EOL whitespace in a text value.
* @param {string | undefined} value - The value to fix.
* @param {(text: string) => void} fixFn - The function to apply the fix.
* @param {boolean} [isRootFirst] - Whether this is the first token of the root.
*/
function fixText (value, fixFn, isRootFirst = false) {
if (!value) return
let fixed = ``
let lastIndex = 0
eachEolWhitespace(
value,
(index) => {
let 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 rule