| 'use strict'; |
| |
| const declarationValueIndex = require('../../utils/declarationValueIndex'); |
| const getDeclarationValue = require('../../utils/getDeclarationValue'); |
| const isSingleLineString = require('../../utils/isSingleLineString'); |
| const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction'); |
| const report = require('../../utils/report'); |
| const ruleMessages = require('../../utils/ruleMessages'); |
| const setDeclarationValue = require('../../utils/setDeclarationValue'); |
| const validateOptions = require('../../utils/validateOptions'); |
| const valueParser = require('postcss-value-parser'); |
| |
| const ruleName = 'function-parentheses-newline-inside'; |
| |
| const messages = ruleMessages(ruleName, { |
| expectedOpening: 'Expected newline after "("', |
| expectedClosing: 'Expected newline before ")"', |
| expectedOpeningMultiLine: 'Expected newline after "(" in a multi-line function', |
| rejectedOpeningMultiLine: 'Unexpected whitespace after "(" in a multi-line function', |
| expectedClosingMultiLine: 'Expected newline before ")" in a multi-line function', |
| rejectedClosingMultiLine: 'Unexpected whitespace before ")" in a multi-line function', |
| }); |
| |
| /** @type {import('stylelint').Rule} */ |
| const rule = (primary, _secondaryOptions, context) => { |
| return (root, result) => { |
| const validOptions = validateOptions(result, ruleName, { |
| actual: primary, |
| possible: ['always', 'always-multi-line', 'never-multi-line'], |
| }); |
| |
| if (!validOptions) { |
| return; |
| } |
| |
| root.walkDecls((decl) => { |
| if (!decl.value.includes('(')) { |
| return; |
| } |
| |
| let hasFixed = false; |
| const declValue = getDeclarationValue(decl); |
| const parsedValue = valueParser(declValue); |
| |
| parsedValue.walk((valueNode) => { |
| if (valueNode.type !== 'function') { |
| return; |
| } |
| |
| if (!isStandardSyntaxFunction(valueNode)) { |
| return; |
| } |
| |
| const functionString = valueParser.stringify(valueNode); |
| const isMultiLine = !isSingleLineString(functionString); |
| const containsNewline = (/** @type {string} */ str) => str.includes('\n'); |
| |
| // Check opening ... |
| |
| const openingIndex = valueNode.sourceIndex + valueNode.value.length + 1; |
| const checkBefore = getCheckBefore(valueNode); |
| |
| if (primary === 'always' && !containsNewline(checkBefore)) { |
| if (context.fix) { |
| hasFixed = true; |
| fixBeforeForAlways(valueNode, context.newline || ''); |
| } else { |
| complain(messages.expectedOpening, openingIndex); |
| } |
| } |
| |
| if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkBefore)) { |
| if (context.fix) { |
| hasFixed = true; |
| fixBeforeForAlways(valueNode, context.newline || ''); |
| } else { |
| complain(messages.expectedOpeningMultiLine, openingIndex); |
| } |
| } |
| |
| if (isMultiLine && primary === 'never-multi-line' && checkBefore !== '') { |
| if (context.fix) { |
| hasFixed = true; |
| fixBeforeForNever(valueNode); |
| } else { |
| complain(messages.rejectedOpeningMultiLine, openingIndex); |
| } |
| } |
| |
| // Check closing ... |
| |
| const closingIndex = valueNode.sourceIndex + functionString.length - 2; |
| const checkAfter = getCheckAfter(valueNode); |
| |
| if (primary === 'always' && !containsNewline(checkAfter)) { |
| if (context.fix) { |
| hasFixed = true; |
| fixAfterForAlways(valueNode, context.newline || ''); |
| } else { |
| complain(messages.expectedClosing, closingIndex); |
| } |
| } |
| |
| if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkAfter)) { |
| if (context.fix) { |
| hasFixed = true; |
| fixAfterForAlways(valueNode, context.newline || ''); |
| } else { |
| complain(messages.expectedClosingMultiLine, closingIndex); |
| } |
| } |
| |
| if (isMultiLine && primary === 'never-multi-line' && checkAfter !== '') { |
| if (context.fix) { |
| hasFixed = true; |
| fixAfterForNever(valueNode); |
| } else { |
| complain(messages.rejectedClosingMultiLine, closingIndex); |
| } |
| } |
| }); |
| |
| if (hasFixed) { |
| setDeclarationValue(decl, parsedValue.toString()); |
| } |
| |
| /** |
| * @param {string} message |
| * @param {number} offset |
| */ |
| function complain(message, offset) { |
| report({ |
| ruleName, |
| result, |
| message, |
| node: decl, |
| index: declarationValueIndex(decl) + offset, |
| }); |
| } |
| }); |
| }; |
| }; |
| |
| /** @typedef {import('postcss-value-parser').FunctionNode} FunctionNode */ |
| |
| /** |
| * @param {FunctionNode} valueNode |
| */ |
| function getCheckBefore(valueNode) { |
| let before = valueNode.before; |
| |
| for (const node of valueNode.nodes) { |
| if (node.type === 'comment') { |
| continue; |
| } |
| |
| if (node.type === 'space') { |
| before += node.value; |
| continue; |
| } |
| |
| break; |
| } |
| |
| return before; |
| } |
| |
| /** |
| * @param {FunctionNode} valueNode |
| */ |
| function getCheckAfter(valueNode) { |
| let after = ''; |
| |
| for (const node of [...valueNode.nodes].reverse()) { |
| if (node.type === 'comment') { |
| continue; |
| } |
| |
| if (node.type === 'space') { |
| after = node.value + after; |
| continue; |
| } |
| |
| break; |
| } |
| |
| after += valueNode.after; |
| |
| return after; |
| } |
| |
| /** |
| * @param {FunctionNode} valueNode |
| * @param {string} newline |
| */ |
| function fixBeforeForAlways(valueNode, newline) { |
| let target; |
| |
| for (const node of valueNode.nodes) { |
| if (node.type === 'comment') { |
| continue; |
| } |
| |
| if (node.type === 'space') { |
| target = node; |
| continue; |
| } |
| |
| break; |
| } |
| |
| if (target) { |
| target.value = newline + target.value; |
| } else { |
| valueNode.before = newline + valueNode.before; |
| } |
| } |
| |
| /** |
| * @param {FunctionNode} valueNode |
| */ |
| function fixBeforeForNever(valueNode) { |
| valueNode.before = ''; |
| |
| for (const node of valueNode.nodes) { |
| if (node.type === 'comment') { |
| continue; |
| } |
| |
| if (node.type === 'space') { |
| node.value = ''; |
| continue; |
| } |
| |
| break; |
| } |
| } |
| |
| /** |
| * @param {FunctionNode} valueNode |
| * @param {string} newline |
| */ |
| function fixAfterForAlways(valueNode, newline) { |
| valueNode.after = newline + valueNode.after; |
| } |
| |
| /** |
| * @param {FunctionNode} valueNode |
| */ |
| function fixAfterForNever(valueNode) { |
| valueNode.after = ''; |
| |
| for (const node of [...valueNode.nodes].reverse()) { |
| if (node.type === 'comment') { |
| continue; |
| } |
| |
| if (node.type === 'space') { |
| node.value = ''; |
| continue; |
| } |
| |
| break; |
| } |
| } |
| |
| rule.ruleName = ruleName; |
| rule.messages = messages; |
| module.exports = rule; |