@stylistic/stylelint-plugin
Version:
A collection of stylistic/formatting Stylelint rules
157 lines (119 loc) • 4.42 kB
JavaScript
import stylelint from "stylelint"
import valueParser from "postcss-value-parser"
import { addNamespace } from "../../utils/addNamespace/index.js"
import { declarationValueIndex } from "../../utils/declarationValueIndex/index.js"
import { getDeclarationValue } from "../../utils/getDeclarationValue/index.js"
import { getRuleDocUrl } from "../../utils/getRuleDocUrl/index.js"
import { setDeclarationValue } from "../../utils/setDeclarationValue/index.js"
import { isBoolean, isNumber } from "../../utils/validateTypes/index.js"
let { utils: { report, ruleMessages, validateOptions } } = stylelint
let shortName = `named-grid-areas-alignment`
export let ruleName = addNamespace(shortName)
export let messages = ruleMessages(ruleName, {
expected: () => `Expected \`grid-template-areas\` value to be aligned`,
})
export let meta = {
url: getRuleDocUrl(shortName),
fixable: true,
}
/** @type {import('stylelint').Rule} */
function rule (primary, secondaryOptions = {}) {
return (root, result) => {
let validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
gap: [isNumber, (value) => value > 1],
alignQuotes: [isBoolean],
},
optional: true,
},
)
if (!validOptions) { return }
let gap = secondaryOptions.gap ?? 1
let alignQuotes = secondaryOptions.alignQuotes ?? false
let referenceGap = ` `.repeat(gap)
root.walkDecls(`grid-template-areas`, (declaration) => {
let declarationValue = getDeclarationValue(declaration)
let parsedValue = valueParser(declarationValue)
let isMultilineDeclaration = declarationValue.includes(`\n`)
let gridRows = parsedValue.nodes.filter((node) => node.type === `string`)
// To compare with the formatted value to determine if there is an error
let originalRows = gridRows.map(({ value }) => value).filter(Boolean)
// The ones to operate with
let rows = gridRows
.map(({ value }) => value.trim().replaceAll(/\s+/g, ` `))
.filter(Boolean)
let maxCellsCount = 0
let table = rows.reduce((acc, row) => {
let cells = row.split(` `)
maxCellsCount = Math.max(maxCellsCount, cells.length)
acc.push(row.split(` `))
return acc
}, [])
let maxLengths = new Array(maxCellsCount).fill(``).reduce((acc, part, index) => {
let parts = table.map((row) => row[index]?.length ?? 0)
acc.push(Math.max(part.length, ...parts))
return acc
}, [])
let maxRowLength = 0
let formatted = table.map((row) => {
let formattedRow = row
.map((cell, index) => isMultilineDeclaration ? cell.padEnd(maxLengths[index], ` `) : cell)
.join(referenceGap)
maxRowLength = Math.max(maxRowLength, formattedRow.length)
return alignQuotes ? formattedRow : formattedRow.trimEnd()
})
if (alignQuotes && isMultilineDeclaration) {
formatted = formatted.map((row) => {
if (row.length === maxRowLength) { return row }
let cleanRowValue = row.trimEnd()
return `${cleanRowValue}${` `.repeat(maxRowLength - cleanRowValue.length)}`
})
}
let isValid = originalRows.every((row, index) => row === formatted[index])
if (isValid) { return }
let extraStartLines = declaration.raws.between.match(/[\r\n?|\n]*/g)
?.reduce((acc, newLineBlock) => acc + newLineBlock.length, 0)
let extraStartColumns = extraStartLines === 0
? declarationValueIndex(declaration) + declaration.source.start.column
: declaration.raws.between.match(/[^\r\n?|\n]+$/)?.[0].length + 1 || 0
report({
message: messages.expected,
node: declaration,
start: {
line: extraStartLines + declaration.source.start.line,
column: extraStartColumns,
},
end: {
line: declaration.source.end.line,
column: declaration.source.end.column,
},
result,
ruleName,
fix () {
let formattedValue = parsedValue.nodes.reduce((acc, node) => {
if (node.type === `string`) {
acc.push(`${node.quote}${formatted.shift()}${node.quote}`)
return acc
}
if (node.type === `comment`) {
acc.push(`/*${node.value}*/`)
return acc
}
acc.push(`${node.before ?? ``}${node.value}${node.after ?? ``}`)
return acc
}, []).join(``)
setDeclarationValue(declaration, formattedValue)
},
})
})
}
}
rule.ruleName = ruleName
rule.messages = messages
rule.meta = meta
export default rule